mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 09:47:50 +03:00
Регистрация и тесты эндпоинтов
This commit is contained in:
190
Cargo.lock
generated
190
Cargo.lock
generated
@@ -49,7 +49,7 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rand",
|
||||
"rand 0.9.0",
|
||||
"sha1",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
@@ -168,7 +168,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2",
|
||||
"time",
|
||||
"time 0.3.40",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
@@ -333,7 +333,7 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"byteorder 1.5.0",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
@@ -364,6 +364,12 @@ version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29b2aa490a8f546381308d68fc79e6bd753cd3ad839f7a7172897f1feedfa175"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -391,7 +397,7 @@ version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "138646b9af2c5d7f1804ea4bf93afc597737d2bd4f7341d67c48b03316976eb1"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"byteorder 1.5.0",
|
||||
"codepage",
|
||||
"encoding_rs",
|
||||
"log",
|
||||
@@ -516,7 +522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"time 0.3.40",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
@@ -714,7 +720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "470eb10efc8646313634c99bb1593f402a6434cbd86e266770c6e39219adb86a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"byteorder 1.5.0",
|
||||
"diesel_derives",
|
||||
"itoa",
|
||||
"pq-sys",
|
||||
@@ -878,6 +884,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -926,6 +938,12 @@ dependencies = [
|
||||
"thread_local",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gcc"
|
||||
version = "0.3.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -1040,6 +1058,16 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winutil",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@@ -1434,6 +1462,12 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.171"
|
||||
@@ -1565,6 +1599,22 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objectid"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22a605e0778d73324c897e7e4bd5903f64639884a99d1bad55bccfd986260063"
|
||||
dependencies = [
|
||||
"byteorder 0.3.13",
|
||||
"hostname",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"quick-error",
|
||||
"rand 0.3.23",
|
||||
"rust-crypto",
|
||||
"rustc-serialize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.1"
|
||||
@@ -1730,6 +1780,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
@@ -1755,6 +1811,29 @@ version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand 0.4.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
|
||||
dependencies = [
|
||||
"fuchsia-cprng",
|
||||
"libc",
|
||||
"rand_core 0.3.1",
|
||||
"rdrand",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.0"
|
||||
@@ -1762,7 +1841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_core 0.9.3",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
@@ -1773,9 +1852,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
|
||||
dependencies = [
|
||||
"rand_core 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
@@ -1805,6 +1899,15 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
|
||||
dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.10"
|
||||
@@ -1907,12 +2010,31 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-crypto"
|
||||
version = "0.2.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
|
||||
dependencies = [
|
||||
"gcc",
|
||||
"libc",
|
||||
"rand 0.3.23",
|
||||
"rustc-serialize",
|
||||
"time 0.1.45",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-serialize"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.3"
|
||||
@@ -1999,6 +2121,7 @@ dependencies = [
|
||||
name = "schedule-parser-rusted"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"actix-http",
|
||||
"actix-web",
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
@@ -2007,9 +2130,12 @@ dependencies = [
|
||||
"dotenvy",
|
||||
"hmac",
|
||||
"jwt",
|
||||
"mime",
|
||||
"objectid",
|
||||
"reqwest",
|
||||
"schedule_parser",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
]
|
||||
@@ -2292,6 +2418,17 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.40"
|
||||
@@ -2548,6 +2685,12 @@ dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
@@ -2644,6 +2787,22 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
@@ -2653,6 +2812,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -2843,6 +3008,15 @@ version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winutil"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
|
||||
18
Cargo.toml
18
Cargo.toml
@@ -8,16 +8,20 @@ edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.10.0"
|
||||
actix-web = "4.10.2"
|
||||
bcrypt = "0.17.0"
|
||||
jwt = "0.16.0"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
chrono = "0.4.40"
|
||||
diesel = { version = "2.2.8", features = ["postgres"] }
|
||||
diesel-derive-enum = { git = "https://github.com/Havunen/diesel-derive-enum.git", features = ["postgres"] }
|
||||
dotenvy = "0.15.7"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
schedule_parser = { path = "./lib/schedule_parser" }
|
||||
chrono = "0.4.40"
|
||||
hmac = "0.12.1"
|
||||
jwt = "0.16.0"
|
||||
mime = "0.3.17"
|
||||
objectid = "0.2.0"
|
||||
reqwest = "0.12.15"
|
||||
schedule_parser = { path = "./lib/schedule_parser" }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
sha2 = "0.10.8"
|
||||
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
|
||||
actix-web = "4.10.2"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::schema::LessonType::Break;
|
||||
use crate::schema::{Day, Lesson, LessonSubGroup, LessonTime, LessonType, ScheduleEntity};
|
||||
use crate::LessonParseResult::{Lessons, Street};
|
||||
use calamine::{open_workbook_from_rs, Reader, Xls};
|
||||
use crate::schema::LessonType::Break;
|
||||
use crate::schema::{
|
||||
Day, Lesson, LessonSubGroup, LessonTime, LessonType, ParseResult, ScheduleEntry,
|
||||
};
|
||||
use calamine::{Reader, Xls, open_workbook_from_rs};
|
||||
use chrono::{Duration, NaiveDateTime};
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
@@ -471,9 +473,9 @@ fn parse_name_and_subgroups(name: &String) -> (String, Vec<LessonSubGroup>) {
|
||||
}
|
||||
|
||||
fn convert_groups_to_teachers(
|
||||
groups: &HashMap<String, ScheduleEntity>,
|
||||
) -> HashMap<String, ScheduleEntity> {
|
||||
let mut teachers: HashMap<String, ScheduleEntity> = HashMap::new();
|
||||
groups: &HashMap<String, ScheduleEntry>,
|
||||
) -> HashMap<String, ScheduleEntry> {
|
||||
let mut teachers: HashMap<String, ScheduleEntry> = HashMap::new();
|
||||
|
||||
let empty_days: Vec<Day> = groups
|
||||
.values()
|
||||
@@ -510,7 +512,7 @@ fn convert_groups_to_teachers(
|
||||
if !teachers.contains_key(&subgroup.teacher) {
|
||||
teachers.insert(
|
||||
subgroup.teacher.clone(),
|
||||
ScheduleEntity {
|
||||
ScheduleEntry {
|
||||
name: subgroup.teacher.clone(),
|
||||
days: empty_days.to_vec(),
|
||||
},
|
||||
@@ -538,12 +540,7 @@ fn convert_groups_to_teachers(
|
||||
teachers
|
||||
}
|
||||
|
||||
pub fn parse_xls(
|
||||
buffer: &Vec<u8>,
|
||||
) -> (
|
||||
HashMap<String, ScheduleEntity>,
|
||||
HashMap<String, ScheduleEntity>,
|
||||
) {
|
||||
pub fn parse_xls(buffer: &Vec<u8>) -> ParseResult {
|
||||
let cursor = Cursor::new(&buffer);
|
||||
let mut workbook: Xls<_> = open_workbook_from_rs(cursor).expect("Can't open workbook");
|
||||
|
||||
@@ -556,13 +553,13 @@ pub fn parse_xls(
|
||||
|
||||
let (days_markup, groups_markup) = parse_skeleton(&worksheet);
|
||||
|
||||
let mut groups: HashMap<String, ScheduleEntity> = HashMap::new();
|
||||
let mut groups: HashMap<String, ScheduleEntry> = HashMap::new();
|
||||
let mut days_times: Vec<Vec<InternalTime>> = Vec::new();
|
||||
|
||||
let saturday_end_row = worksheet.end().unwrap().0;
|
||||
|
||||
for group_markup in groups_markup {
|
||||
let mut group = ScheduleEntity {
|
||||
let mut group = ScheduleEntry {
|
||||
name: group_markup.name,
|
||||
days: Vec::new(),
|
||||
};
|
||||
@@ -686,7 +683,10 @@ pub fn parse_xls(
|
||||
groups.insert(group.name.clone(), group);
|
||||
}
|
||||
|
||||
(convert_groups_to_teachers(&groups), groups)
|
||||
ParseResult {
|
||||
teachers: convert_groups_to_teachers(&groups),
|
||||
groups,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -698,7 +698,7 @@ mod tests {
|
||||
let buffer: Vec<u8> = include_bytes!("../../../../schedule.xls").to_vec();
|
||||
let result = parse_xls(&buffer);
|
||||
|
||||
assert_ne!(result.0.len(), 0);
|
||||
assert_ne!(result.1.len(), 0);
|
||||
assert_ne!(result.groups.len(), 0);
|
||||
assert_ne!(result.teachers.len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,84 +24,104 @@ pub enum LessonType {
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LessonSubGroup {
|
||||
/**
|
||||
* Номер подгруппы.
|
||||
*/
|
||||
pub number: u8,
|
||||
|
||||
/**
|
||||
* Кабинет, если присутствует.
|
||||
*/
|
||||
pub cabinet: Option<String>,
|
||||
|
||||
/**
|
||||
* Фио преподавателя.
|
||||
*/
|
||||
pub teacher: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Lesson {
|
||||
/**
|
||||
* Тип занятия
|
||||
* Тип занятия.
|
||||
*/
|
||||
#[serde(rename = "type")]
|
||||
pub lesson_type: LessonType,
|
||||
|
||||
/**
|
||||
* Индексы пар, если присутствуют
|
||||
* Индексы пар, если присутствуют.
|
||||
*/
|
||||
#[serde(rename = "defaultRange")]
|
||||
pub default_range: Option<[u8; 2]>,
|
||||
|
||||
/**
|
||||
* Название занятия
|
||||
* Название занятия.
|
||||
*/
|
||||
pub name: Option<String>,
|
||||
|
||||
/**
|
||||
* Начало и конец занятия
|
||||
* Начало и конец занятия.
|
||||
*/
|
||||
pub time: LessonTime,
|
||||
|
||||
/**
|
||||
* Подгруппы
|
||||
* Подгруппы.
|
||||
*/
|
||||
#[serde(rename = "subGroups")]
|
||||
pub subgroups: Option<Vec<LessonSubGroup>>,
|
||||
|
||||
/**
|
||||
* Группа (только для расписания преподавателей)
|
||||
* Группа, если это расписание для преподавателей.
|
||||
*/
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Day {
|
||||
/**
|
||||
* День недели.
|
||||
*/
|
||||
pub name: String,
|
||||
|
||||
/**
|
||||
* Адрес другого корпуса.
|
||||
*/
|
||||
pub street: Option<String>,
|
||||
|
||||
/**
|
||||
* Дата.
|
||||
*/
|
||||
pub date: DateTime<Utc>,
|
||||
|
||||
/**
|
||||
* Список пар в этот день.
|
||||
*/
|
||||
pub lessons: Vec<Lesson>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ScheduleEntity {
|
||||
pub struct ScheduleEntry {
|
||||
/**
|
||||
* Название группы или ФИО преподавателя.
|
||||
*/
|
||||
pub name: String,
|
||||
|
||||
/**
|
||||
* Список из шести дней.
|
||||
*/
|
||||
pub days: Vec<Day>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Schedule {
|
||||
#[serde(rename = "updatedAt")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub struct ParseResult {
|
||||
/**
|
||||
* Список групп.
|
||||
* Ключом является название группы.
|
||||
*/
|
||||
pub groups: HashMap<String, ScheduleEntry>,
|
||||
|
||||
pub groups: HashMap<String, ScheduleEntity>,
|
||||
|
||||
#[serde(rename = "updatedGroups")]
|
||||
pub updated_groups: Vec<Vec<usize>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct TeacherSchedule {
|
||||
#[serde(rename = "updatedAt")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
pub teacher: ScheduleEntity,
|
||||
|
||||
pub updated: Vec<usize>,
|
||||
/**
|
||||
* Список преподавателей.
|
||||
* Ключом является ФИО преподавателя.
|
||||
*/
|
||||
pub teachers: HashMap<String, ScheduleEntry>,
|
||||
}
|
||||
|
||||
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