Регистрация и тесты эндпоинтов

This commit is contained in:
2025-03-23 06:11:13 +04:00
parent 844c89a365
commit a95494d3be
16 changed files with 822 additions and 176 deletions

39
src/app_state.rs Normal file
View 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)),
),
})
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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()

View File

@@ -1,2 +1,3 @@
mod schema;
pub mod sign_in;
mod schema;
pub mod sign_up;

View File

@@ -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
}
}
}

View File

@@ -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
View 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);
}
}

View File

@@ -1 +1,2 @@
pub mod auth;
pub mod auth;
mod schema;

69
src/routes/schema.rs Normal file
View 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
View 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()
}
}

View File

@@ -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());