mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 09:47:50 +03:00
Регистрация и тесты эндпоинтов
This commit is contained in:
39
src/app_state.rs
Normal file
39
src/app_state.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use crate::xls_downloader::basic_impl::BasicXlsDownloader;
|
||||
use actix_web::web;
|
||||
use chrono::{DateTime, Utc};
|
||||
use diesel::{Connection, PgConnection};
|
||||
use schedule_parser::schema::ParseResult;
|
||||
use std::env;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
pub struct Schedule {
|
||||
pub etag: String,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub parsed_at: DateTime<Utc>,
|
||||
pub data: ParseResult,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub downloader: Mutex<BasicXlsDownloader>,
|
||||
pub schedule: Mutex<Option<Schedule>>,
|
||||
pub database: Mutex<PgConnection>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn connection(&self) -> MutexGuard<PgConnection> {
|
||||
self.database.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_state() -> web::Data<AppState> {
|
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
|
||||
web::Data::new(AppState {
|
||||
downloader: Mutex::new(BasicXlsDownloader::new()),
|
||||
schedule: Mutex::new(None),
|
||||
database: Mutex::new(
|
||||
PgConnection::establish(&database_url)
|
||||
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url)),
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
pub mod users {
|
||||
use crate::database::models::User;
|
||||
use crate::database::schema::fcm::user_id;
|
||||
use crate::database::schema::users::dsl::users;
|
||||
use crate::database::schema::users::dsl::*;
|
||||
use diesel::{ExpressionMethods, QueryResult};
|
||||
use diesel::{insert_into, ExpressionMethods, QueryResult};
|
||||
use diesel::{PgConnection, SelectableHelper};
|
||||
use diesel::{QueryDsl, RunQueryDsl};
|
||||
use std::ops::DerefMut;
|
||||
@@ -31,4 +30,42 @@ pub mod users {
|
||||
.select(User::as_select())
|
||||
.first(con)
|
||||
}
|
||||
|
||||
pub fn contains_by_username(connection: &Mutex<PgConnection>, _username: String) -> bool {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
match users
|
||||
.filter(username.eq(_username))
|
||||
.count()
|
||||
.get_result::<i64>(con)
|
||||
{
|
||||
Ok(count) => count > 0,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_by_username(connection: &Mutex<PgConnection>, _username: String) -> bool {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
match diesel::delete(users.filter(username.eq(_username))).execute(con) {
|
||||
Ok(count) => count > 0,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(connection: &Mutex<PgConnection>, user: &User) -> QueryResult<usize> {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
insert_into(users).values(user).execute(con)
|
||||
}
|
||||
|
||||
pub fn insert_or_ignore(connection: &Mutex<PgConnection>, user: &User) -> QueryResult<usize> {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
insert_into(users).values(user).on_conflict_do_nothing().execute(con)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use diesel::prelude::*;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(diesel_derive_enum::DbEnum, Serialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(diesel_derive_enum::DbEnum, Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[ExistingTypePath = "crate::database::schema::sql_types::UserRole"]
|
||||
#[DbValueStyle = "UPPERCASE"]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
@@ -11,7 +11,7 @@ pub enum UserRole {
|
||||
Admin,
|
||||
}
|
||||
|
||||
#[derive(Identifiable, AsChangeset, Queryable, Selectable, Serialize)]
|
||||
#[derive(Identifiable, AsChangeset, Queryable, Selectable, Serialize, Insertable, Debug)]
|
||||
#[diesel(table_name = crate::database::schema::users)]
|
||||
#[diesel(treat_none_as_null = true)]
|
||||
pub struct User {
|
||||
|
||||
47
src/main.rs
47
src/main.rs
@@ -1,60 +1,27 @@
|
||||
use crate::app_state::{AppState, app_state};
|
||||
use crate::routes::auth::sign_in::sign_in;
|
||||
use crate::xls_downloader::basic_impl::BasicXlsDownloader;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use chrono::{DateTime, Utc};
|
||||
use diesel::{Connection, PgConnection};
|
||||
use dotenvy::dotenv;
|
||||
use schedule_parser::schema::ScheduleEntity;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
mod app_state;
|
||||
mod database;
|
||||
mod routes;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_env;
|
||||
|
||||
mod utility;
|
||||
mod xls_downloader;
|
||||
|
||||
pub struct AppState {
|
||||
downloader: Mutex<BasicXlsDownloader>,
|
||||
schedule: Mutex<
|
||||
Option<(
|
||||
String,
|
||||
DateTime<Utc>,
|
||||
(
|
||||
HashMap<String, ScheduleEntity>,
|
||||
HashMap<String, ScheduleEntity>,
|
||||
),
|
||||
)>,
|
||||
>,
|
||||
database: Mutex<PgConnection>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn connection(&self) -> MutexGuard<PgConnection> {
|
||||
self.database.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
|
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
|
||||
let data = web::Data::new(AppState {
|
||||
downloader: Mutex::new(BasicXlsDownloader::new()),
|
||||
schedule: Mutex::new(None),
|
||||
database: Mutex::new(
|
||||
PgConnection::establish(&database_url)
|
||||
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url)),
|
||||
),
|
||||
});
|
||||
|
||||
HttpServer::new(move || {
|
||||
let schedule_scope = web::scope("/auth").service(sign_in);
|
||||
let api_scope = web::scope("/api/v1").service(schedule_scope);
|
||||
|
||||
App::new().app_data(data.clone()).service(api_scope)
|
||||
App::new().app_data(move || app_state()).service(api_scope)
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))
|
||||
.unwrap()
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/test_env.rs
Normal file
27
src/test_env.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use crate::app_state::{AppState, app_state};
|
||||
use actix_web::dev::{HttpServiceFactory, Service, ServiceResponse};
|
||||
use actix_web::{App, test, web};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub fn test_env() {
|
||||
dotenvy::from_path(".env.test").expect("Failed to load test environment file");
|
||||
}
|
||||
|
||||
pub async fn test_app<F>(
|
||||
app_state: web::Data<AppState>,
|
||||
factory: F,
|
||||
) -> impl Service<actix_http::Request, Response = ServiceResponse, Error = actix_web::Error>
|
||||
where
|
||||
F: HttpServiceFactory + 'static,
|
||||
{
|
||||
test::init_service(App::new().app_data(app_state).service(factory)).await
|
||||
}
|
||||
|
||||
pub fn static_app_state() -> web::Data<AppState> {
|
||||
static STATE: LazyLock<web::Data<AppState>> = LazyLock::new(|| app_state());
|
||||
|
||||
STATE.clone()
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,9 @@ pub fn verify_and_decode(token: &String) -> Result<String, VerifyError> {
|
||||
Ok(claims.get("id").cloned().unwrap())
|
||||
}
|
||||
Err(err) => Err(match err {
|
||||
jwt::Error::InvalidSignature => VerifyError::InvalidSignature,
|
||||
jwt::Error::InvalidSignature | jwt::Error::RustCryptoMac(_) => {
|
||||
VerifyError::InvalidSignature
|
||||
}
|
||||
jwt::Error::Format | jwt::Error::Base64(_) | jwt::Error::NoClaimsComponent => {
|
||||
VerifyError::InvalidToken
|
||||
}
|
||||
@@ -86,18 +88,18 @@ pub fn encode(id: &String) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use dotenvy::dotenv;
|
||||
use crate::test_env::tests::test_env;
|
||||
|
||||
#[test]
|
||||
fn test_encode() {
|
||||
dotenv().unwrap();
|
||||
test_env();
|
||||
|
||||
assert_eq!(encode(&"test".to_string()).is_empty(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_invalid_token() {
|
||||
dotenv().unwrap();
|
||||
test_env();
|
||||
|
||||
let token = "".to_string();
|
||||
let result = verify_and_decode(&token);
|
||||
@@ -108,20 +110,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_decode_invalid_signature() {
|
||||
dotenv().unwrap();
|
||||
test_env();
|
||||
|
||||
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxODY4ODEyOTI4IiwiaWF0IjoiMTc0MjY2ODkyOCIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.DQYFYF-3DoJgCLOVdAWa47nUaCJAh16DXj-ChNSSmWz".to_string();
|
||||
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNjE2NTI2Mzc2IiwiaWF0IjoiMTQ5MDM4MjM3NiIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.Qc2LbMJTvl2hWzDM2XyQv4m9lIqR84COAESQAieUxz8".to_string();
|
||||
let result = verify_and_decode(&token);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.err().unwrap(), VerifyError::InvalidToken);
|
||||
assert_eq!(result.err().unwrap(), VerifyError::InvalidSignature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_expired() {
|
||||
dotenv().unwrap();
|
||||
test_env();
|
||||
|
||||
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNjE2NTI2Mzc2IiwiaWF0IjoiMTQ5MDM4MjM3NiIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.Qc2LbMJTvl2hWzDM2XyQv4m9lIqR84COAESQAieUxz8".to_string();
|
||||
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6IjAiLCJleHAiOiIwIn0.GBsVYvnZIfHXt00t-qmAdUMyHSyWOBtC0Mrxwg1HQOM".to_string();
|
||||
let result = verify_and_decode(&token);
|
||||
|
||||
assert!(result.is_err());
|
||||
@@ -130,9 +132,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_decode_ok() {
|
||||
dotenv().unwrap();
|
||||
test_env();
|
||||
|
||||
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxODY4ODEyOTI4IiwiaWF0IjoiMTc0MjY2ODkyOCIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.DQYFYF-3DoJgCLOVdAWa47nUaCJAh16DXj-ChNSSmWw".to_string();
|
||||
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6Ijk5OTk5OTk5OTkiLCJleHAiOiI5OTk5OTk5OTk5In0.o1vN-ze5iaJrnlHqe7WARXMBhhzjxTjTKkjlmTGEnOI".to_string();
|
||||
let result = verify_and_decode(&token);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
Reference in New Issue
Block a user