claude rework
This commit is contained in:
+10
-4
@@ -3,29 +3,36 @@ extern crate jwt;
|
||||
extern crate sha2;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
|
||||
use actix_web::HttpRequest;
|
||||
use dotenv::dotenv;
|
||||
use hmac::{Hmac, Mac};
|
||||
use jwt::{Header, SignWithKey, Token, VerifyWithKey};
|
||||
use sha2::Sha256;
|
||||
|
||||
pub struct JwtToken {
|
||||
pub user_id: i32,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
fn signing_key() -> HmacSha256 {
|
||||
dotenv().ok();
|
||||
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
HmacSha256::new_from_slice(secret.as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
impl JwtToken {
|
||||
pub fn encode(user_id: i32) -> String {
|
||||
let key: HmacSha256 = HmacSha256::new_from_slice(b"secret").unwrap();
|
||||
let key: HmacSha256 = signing_key();
|
||||
let mut claims = BTreeMap::new();
|
||||
claims.insert("user_id", user_id);
|
||||
claims.sign_with_key(&key).unwrap()
|
||||
}
|
||||
|
||||
pub fn decode(encoded_token: String) -> Result<JwtToken, &'static str> {
|
||||
let key: HmacSha256 = HmacSha256::new_from_slice(b"secret").unwrap();
|
||||
let key: HmacSha256 = signing_key();
|
||||
let token_str: &str = encoded_token.as_str();
|
||||
let token: Result<Token<Header, BTreeMap<String, i32>, jwt::Verified>, jwt::Error> =
|
||||
VerifyWithKey::verify_with_key(token_str, &key);
|
||||
@@ -36,7 +43,6 @@ impl JwtToken {
|
||||
let claims = token.claims();
|
||||
Ok(JwtToken {
|
||||
user_id: claims["user_id"],
|
||||
body: encoded_token,
|
||||
})
|
||||
}
|
||||
Err(_err) => Err("could not decode token"),
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ pub mod processes;
|
||||
use crate::auth::processes::check_password;
|
||||
use crate::auth::processes::extract_header_token;
|
||||
|
||||
pub fn process_token(request: &ServiceRequest) -> Result<String, &'static str> {
|
||||
pub fn process_token(request: &ServiceRequest) -> Result<i32, &'static str> {
|
||||
match extract_header_token(request) {
|
||||
Ok(token) => check_password(token),
|
||||
Err(message) => Err(message),
|
||||
@@ -26,7 +26,7 @@ mod mod_test {
|
||||
.to_srv_request();
|
||||
|
||||
match process_token(&request) {
|
||||
Ok(message) => assert_eq!("passed", message),
|
||||
Ok(user_id) => assert_eq!(32, user_id),
|
||||
Err(_) => panic!("process token failed"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::jwt;
|
||||
use actix_web::dev::ServiceRequest;
|
||||
|
||||
pub fn check_password(password: String) -> Result<String, &'static str> {
|
||||
pub fn check_password(password: String) -> Result<i32, &'static str> {
|
||||
match jwt::JwtToken::decode(password) {
|
||||
Ok(_token) => Ok(String::from("passed")),
|
||||
Ok(token) => Ok(token.user_id),
|
||||
Err(message) => Err(message),
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ mod processes_test {
|
||||
let result = check_password(password_string);
|
||||
|
||||
match result {
|
||||
Ok(check) => assert_eq!("passed", check),
|
||||
Ok(user_id) => assert_eq!(32, user_id),
|
||||
_ => panic!("Check correct password failed."),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
|
||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
|
||||
pub fn establish_connection() -> PgConnection {
|
||||
dotenv().ok();
|
||||
|
||||
@@ -10,3 +13,9 @@ pub fn establish_connection() -> PgConnection {
|
||||
PgConnection::establish(&database_url)
|
||||
.unwrap_or_else(|e| panic!("Error connecting to database {}: {}", database_url, e))
|
||||
}
|
||||
|
||||
pub fn run_migrations(connection: &mut PgConnection) {
|
||||
connection
|
||||
.run_pending_migrations(MIGRATIONS)
|
||||
.expect("Failed to run database migrations");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, Responder};
|
||||
use reqwest::StatusCode;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::reader::structs::feed::FeedAggregate;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod articles;
|
||||
pub mod login;
|
||||
pub mod new_feed;
|
||||
pub mod new_feed_item;
|
||||
pub mod new_user;
|
||||
pub mod read_feed_item;
|
||||
pub mod readable;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NewFeedItemSchema {
|
||||
pub content: String,
|
||||
pub feed_id: i32,
|
||||
pub url: String,
|
||||
pub title: String,
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, Responder};
|
||||
use reqwest::StatusCode;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
+24
-3
@@ -1,22 +1,39 @@
|
||||
extern crate diesel;
|
||||
extern crate dotenv;
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_service::Service;
|
||||
use actix_web::{App, HttpResponse, HttpServer};
|
||||
use dotenv::dotenv;
|
||||
use futures::future::{ok, Either};
|
||||
use std::env;
|
||||
mod auth;
|
||||
mod database;
|
||||
mod json_serialization;
|
||||
mod models;
|
||||
mod reader;
|
||||
mod schema;
|
||||
#[cfg(test)]
|
||||
mod test_helpers;
|
||||
mod views;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv().ok();
|
||||
env_logger::init();
|
||||
|
||||
HttpServer::new(|| {
|
||||
database::run_migrations(&mut database::establish_connection());
|
||||
|
||||
let frontend_origin =
|
||||
env::var("FRONTEND_ORIGIN").unwrap_or_else(|_| String::from("http://localhost:5173"));
|
||||
|
||||
HttpServer::new(move || {
|
||||
let cors = Cors::default()
|
||||
.allowed_origin(&frontend_origin)
|
||||
.allow_any_method()
|
||||
.allow_any_header()
|
||||
.supports_credentials();
|
||||
|
||||
let app = App::new()
|
||||
.wrap_fn(|req, srv| {
|
||||
let mut passed: bool;
|
||||
@@ -25,7 +42,10 @@ async fn main() -> std::io::Result<()> {
|
||||
log::info!("Request Url: {}", request_url);
|
||||
if req.path().contains("/article/") {
|
||||
match auth::process_token(&req) {
|
||||
Ok(_token) => passed = true,
|
||||
Ok(user_id) => {
|
||||
log::info!("Authenticated user {} for {}", user_id, request_url);
|
||||
passed = true;
|
||||
}
|
||||
Err(_message) => passed = false,
|
||||
}
|
||||
} else {
|
||||
@@ -52,10 +72,11 @@ async fn main() -> std::io::Result<()> {
|
||||
Ok(result)
|
||||
}
|
||||
})
|
||||
.wrap(cors)
|
||||
.configure(views::views_factory);
|
||||
app
|
||||
})
|
||||
.bind("127.0.0.1:8001")?
|
||||
.bind("0.0.0.0:8001")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@ pub struct User {
|
||||
|
||||
impl User {
|
||||
pub fn verify(self, password: String) -> bool {
|
||||
return verify(password.as_str(), &self.password).unwrap();
|
||||
verify(password.as_str(), &self.password).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,3 +42,28 @@ pub async fn add(new_feed: web::Json<NewFeedSchema>) -> HttpResponse {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{test, web, App};
|
||||
|
||||
use super::add;
|
||||
use crate::test_helpers::unique_suffix;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn add_fails_for_unfetchable_feed_url() {
|
||||
let app = test::init_service(App::new().route("/add", web::post().to(add))).await;
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/add")
|
||||
.set_json(serde_json::json!({
|
||||
"title": "Bad feed",
|
||||
"url": format!("not-a-valid-url-{}", unique_suffix()),
|
||||
"user_id": 1
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::NOT_FOUND, resp.status());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,3 +72,44 @@ pub async fn get(path: web::Path<JsonUser>, req: HttpRequest) -> impl Responder
|
||||
|
||||
articles.respond_to(&request)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{test, web, App};
|
||||
|
||||
use super::get;
|
||||
use crate::database::establish_connection;
|
||||
use crate::test_helpers::{
|
||||
delete_feed, delete_feed_item, delete_user, insert_feed, insert_feed_item, insert_user,
|
||||
};
|
||||
|
||||
#[actix_web::test]
|
||||
async fn get_returns_only_unread_items() {
|
||||
let mut connection = establish_connection();
|
||||
let user = insert_user(&mut connection, "secret");
|
||||
let feed = insert_feed(&mut connection, user.id);
|
||||
let unread = insert_feed_item(&mut connection, feed.id, false);
|
||||
let read = insert_feed_item(&mut connection, feed.id, true);
|
||||
|
||||
let app =
|
||||
test::init_service(App::new().route("/get/{user_id}", web::get().to(get))).await;
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/get/{}", user.id))
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::OK, resp.status());
|
||||
|
||||
let body = test::read_body(resp).await;
|
||||
let body_str = String::from_utf8(body.to_vec()).unwrap();
|
||||
|
||||
assert!(body_str.contains(&unread.title));
|
||||
assert!(!body_str.contains(&read.title));
|
||||
|
||||
delete_feed_item(&mut connection, unread.id);
|
||||
delete_feed_item(&mut connection, read.id);
|
||||
delete_feed(&mut connection, feed.id);
|
||||
delete_user(&mut connection, user.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,3 +29,55 @@ pub async fn mark_read(_req: HttpRequest, path: web::Path<ReadItem>) -> impl Res
|
||||
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{test, web, App};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use super::mark_read;
|
||||
use crate::database::establish_connection;
|
||||
use crate::models::feed_item::rss_feed_item::FeedItem;
|
||||
use crate::schema::feed_item;
|
||||
use crate::test_helpers::{
|
||||
delete_feed, delete_feed_item, delete_user, insert_feed, insert_feed_item, insert_user,
|
||||
};
|
||||
|
||||
#[actix_web::test]
|
||||
async fn mark_read_flips_the_read_flag() {
|
||||
let mut connection = establish_connection();
|
||||
let user = insert_user(&mut connection, "secret");
|
||||
let feed = insert_feed(&mut connection, user.id);
|
||||
let item = insert_feed_item(&mut connection, feed.id, false);
|
||||
|
||||
let app =
|
||||
test::init_service(App::new().route("/read/{id}", web::put().to(mark_read))).await;
|
||||
let req = test::TestRequest::put()
|
||||
.uri(&format!("/read/{}", item.id))
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::OK, resp.status());
|
||||
|
||||
let updated: FeedItem = feed_item::table
|
||||
.find(item.id)
|
||||
.first(&mut connection)
|
||||
.unwrap();
|
||||
assert!(updated.read);
|
||||
|
||||
delete_feed_item(&mut connection, item.id);
|
||||
delete_feed(&mut connection, feed.id);
|
||||
delete_user(&mut connection, user.id);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn mark_read_returns_not_found_for_unknown_id() {
|
||||
let app =
|
||||
test::init_service(App::new().route("/read/{id}", web::put().to(mark_read))).await;
|
||||
let req = test::TestRequest::put().uri("/read/999999999").to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::NOT_FOUND, resp.status());
|
||||
}
|
||||
}
|
||||
|
||||
+82
-25
@@ -19,38 +19,19 @@ use rss::Item;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
fn get_date(date_str: &str) -> Result<NaiveDateTime, chrono::ParseError> {
|
||||
// let format_string = "%a, %d %b %Y %H:%M:%S %z";
|
||||
let format_string = "%Y-%m-%dT%H:%M:%S%Z";
|
||||
|
||||
let result = parse(date_str).unwrap();
|
||||
log::info!("Date: {:?}", result);
|
||||
|
||||
match NaiveDateTime::parse_from_str(&result.to_string(), format_string) {
|
||||
Ok(r) => Ok(r),
|
||||
Err(_) => {
|
||||
let datetime = DateTime::parse_from_rfc2822(date_str);
|
||||
match datetime {
|
||||
Ok(r) => NaiveDateTime::parse_from_str(&r.to_rfc3339(), format_string),
|
||||
Err(_) => match DateTime::parse_from_rfc2822(date_str) {
|
||||
Ok(r) => NaiveDateTime::parse_from_str(&r.to_rfc3339(), format_string),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
if let Ok(result) = parse(date_str) {
|
||||
log::info!("Date: {:?}", result);
|
||||
return Ok(result.with_timezone(&Local).naive_local());
|
||||
}
|
||||
|
||||
DateTime::parse_from_rfc2822(date_str).map(|dt| dt.with_timezone(&Local).naive_local())
|
||||
}
|
||||
|
||||
fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) {
|
||||
let item_title = item.title.clone().unwrap();
|
||||
log::info!("Create feed item: {}", item_title);
|
||||
|
||||
let base_content: &str = match item.content() {
|
||||
Some(c) => c,
|
||||
None => match item.description() {
|
||||
Some(c) => c,
|
||||
None => "",
|
||||
},
|
||||
};
|
||||
let base_content: &str = item.content().or(item.description()).unwrap_or_default();
|
||||
|
||||
let frag = Html::parse_fragment(base_content);
|
||||
let mut content = "".to_string();
|
||||
@@ -133,3 +114,79 @@ pub async fn sync(_req: HttpRequest, data: web::Json<JsonUser>) -> impl Responde
|
||||
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::feed::new_feed::NewFeed;
|
||||
use crate::models::user::new_user::NewUser;
|
||||
use crate::models::user::rss_user::User;
|
||||
use crate::schema::users;
|
||||
use crate::test_helpers::unique_suffix;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn get_date_parses_iso8601_dates() {
|
||||
assert!(get_date("2024-01-01T12:00:00Z").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_date_parses_rfc2822_dates() {
|
||||
assert!(get_date("Tue, 03 Jun 2025 10:00:00 GMT").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_date_returns_err_for_unparseable_dates() {
|
||||
assert!(get_date("not-a-date").is_err());
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn create_feed_item_does_not_duplicate_existing_items() {
|
||||
let mut connection = establish_connection();
|
||||
let suffix = unique_suffix();
|
||||
|
||||
let new_user = NewUser::new(
|
||||
format!("sync_test_{suffix}"),
|
||||
format!("sync_{suffix}@example.test"),
|
||||
"secret".to_string(),
|
||||
);
|
||||
let user: User = diesel::insert_into(users::table)
|
||||
.values(&new_user)
|
||||
.get_result(&mut connection)
|
||||
.unwrap();
|
||||
|
||||
let new_feed = NewFeed::new(
|
||||
format!("Sync test feed {suffix}"),
|
||||
format!("https://example.test/feed/{suffix}"),
|
||||
user.id,
|
||||
);
|
||||
let feed: Feed = diesel::insert_into(feed::table)
|
||||
.values(&new_feed)
|
||||
.get_result(&mut connection)
|
||||
.unwrap();
|
||||
|
||||
let mut item = Item::default();
|
||||
item.set_title(Some(format!("Sync test article {suffix}")));
|
||||
item.set_link(Some(format!("https://example.test/article/{suffix}")));
|
||||
item.set_content(Some("<p>Hello world</p>".to_string()));
|
||||
|
||||
create_feed_item(item.clone(), &feed, &mut connection);
|
||||
create_feed_item(item, &feed, &mut connection);
|
||||
|
||||
let items: Vec<FeedItem> = feed_item::table
|
||||
.filter(feed_id.eq(feed.id))
|
||||
.load(&mut connection)
|
||||
.unwrap();
|
||||
assert_eq!(1, items.len(), "duplicate feed items should not be created");
|
||||
|
||||
diesel::delete(feed_item::table.filter(feed_id.eq(feed.id)))
|
||||
.execute(&mut connection)
|
||||
.ok();
|
||||
diesel::delete(feed::table.filter(feed::id.eq(feed.id)))
|
||||
.execute(&mut connection)
|
||||
.ok();
|
||||
diesel::delete(users::table.filter(users::id.eq(user.id)))
|
||||
.execute(&mut connection)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use diesel::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::feed::new_feed::NewFeed;
|
||||
use crate::models::feed::rss_feed::Feed;
|
||||
use crate::models::feed_item::new_feed_item::NewFeedItem;
|
||||
use crate::models::feed_item::rss_feed_item::FeedItem;
|
||||
use crate::models::user::new_user::NewUser;
|
||||
use crate::models::user::rss_user::User;
|
||||
use crate::schema::{feed, feed_item, users};
|
||||
|
||||
// Test fixtures are written through the same models/schema as the app and
|
||||
// cleaned up explicitly afterwards (rather than wrapped in a rolled-back
|
||||
// transaction), because every handler opens its own DB connection via
|
||||
// `establish_connection`, so a transaction held by the test would not be
|
||||
// visible to the handler under test. Random suffixes keep parallel test runs
|
||||
// from colliding on the unique username/email/url constraints.
|
||||
|
||||
pub fn unique_suffix() -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
pub fn insert_user(connection: &mut PgConnection, password: &str) -> User {
|
||||
let suffix = unique_suffix();
|
||||
let new_user = NewUser::new(
|
||||
format!("test_user_{suffix}"),
|
||||
format!("test_{suffix}@example.test"),
|
||||
password.to_string(),
|
||||
);
|
||||
diesel::insert_into(users::table)
|
||||
.values(&new_user)
|
||||
.get_result(connection)
|
||||
.expect("failed to insert test user")
|
||||
}
|
||||
|
||||
pub fn delete_user(connection: &mut PgConnection, user_id: i32) {
|
||||
diesel::delete(users::table.filter(users::id.eq(user_id)))
|
||||
.execute(connection)
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn insert_feed(connection: &mut PgConnection, owner_id: i32) -> Feed {
|
||||
let suffix = unique_suffix();
|
||||
let new_feed = NewFeed::new(
|
||||
format!("Test feed {suffix}"),
|
||||
format!("https://example.test/feed/{suffix}"),
|
||||
owner_id,
|
||||
);
|
||||
diesel::insert_into(feed::table)
|
||||
.values(&new_feed)
|
||||
.get_result(connection)
|
||||
.expect("failed to insert test feed")
|
||||
}
|
||||
|
||||
pub fn delete_feed(connection: &mut PgConnection, id: i32) {
|
||||
diesel::delete(feed::table.filter(feed::id.eq(id)))
|
||||
.execute(connection)
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn insert_feed_item(connection: &mut PgConnection, owning_feed_id: i32, read: bool) -> FeedItem {
|
||||
let suffix = unique_suffix();
|
||||
let new_item = NewFeedItem::new(
|
||||
owning_feed_id,
|
||||
format!("Content {suffix}"),
|
||||
format!("Title {suffix}"),
|
||||
format!("https://example.test/article/{suffix}"),
|
||||
None,
|
||||
);
|
||||
let item: FeedItem = diesel::insert_into(feed_item::table)
|
||||
.values(&new_item)
|
||||
.get_result(connection)
|
||||
.expect("failed to insert test feed item");
|
||||
|
||||
if read {
|
||||
diesel::update(&item)
|
||||
.set(feed_item::read.eq(true))
|
||||
.execute(connection)
|
||||
.expect("failed to mark test feed item as read");
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
pub fn delete_feed_item(connection: &mut PgConnection, id: i32) {
|
||||
diesel::delete(feed_item::table.filter(feed_item::id.eq(id)))
|
||||
.execute(connection)
|
||||
.ok();
|
||||
}
|
||||
@@ -43,3 +43,69 @@ pub async fn login(credentials: web::Json<Login>) -> HttpResponse {
|
||||
false => HttpResponse::Unauthorized().await.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{test, web, App};
|
||||
|
||||
use super::login;
|
||||
use crate::database::establish_connection;
|
||||
use crate::test_helpers::{delete_user, insert_user, unique_suffix};
|
||||
|
||||
#[actix_web::test]
|
||||
async fn login_succeeds_with_correct_credentials() {
|
||||
let mut connection = establish_connection();
|
||||
let user = insert_user(&mut connection, "correct-password");
|
||||
|
||||
let app = test::init_service(App::new().route("/login", web::post().to(login))).await;
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/login")
|
||||
.set_json(serde_json::json!({
|
||||
"username": user.username,
|
||||
"password": "correct-password"
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::OK, resp.status());
|
||||
assert!(resp.headers().contains_key("token"));
|
||||
|
||||
delete_user(&mut connection, user.id);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn login_fails_with_wrong_password() {
|
||||
let mut connection = establish_connection();
|
||||
let user = insert_user(&mut connection, "correct-password");
|
||||
|
||||
let app = test::init_service(App::new().route("/login", web::post().to(login))).await;
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/login")
|
||||
.set_json(serde_json::json!({
|
||||
"username": user.username,
|
||||
"password": "wrong-password"
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::UNAUTHORIZED, resp.status());
|
||||
|
||||
delete_user(&mut connection, user.id);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn login_fails_for_unknown_user() {
|
||||
let app = test::init_service(App::new().route("/login", web::post().to(login))).await;
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/login")
|
||||
.set_json(serde_json::json!({
|
||||
"username": format!("does-not-exist-{}", unique_suffix()),
|
||||
"password": "whatever"
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::NOT_FOUND, resp.status());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
pub async fn logout() -> String {
|
||||
"logout view".to_string()
|
||||
use actix_web::HttpResponse;
|
||||
|
||||
// JWT auth is stateless and there is no token blacklist, so logging out is
|
||||
// purely a client-side action (discarding the stored token). This endpoint
|
||||
// exists so the frontend has something to call and gets a clean response.
|
||||
pub async fn logout() -> HttpResponse {
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{test, web, App};
|
||||
|
||||
use super::logout;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn logout_returns_ok() {
|
||||
let app = test::init_service(App::new().route("/logout", web::post().to(logout))).await;
|
||||
let req = test::TestRequest::post().uri("/logout").to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::OK, resp.status());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,3 +23,61 @@ pub async fn create(new_user: web::Json<NewUserSchema>) -> HttpResponse {
|
||||
Err(_) => HttpResponse::Conflict().await.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{test, web, App};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use super::create;
|
||||
use crate::database::establish_connection;
|
||||
use crate::schema::users;
|
||||
use crate::test_helpers::{delete_user, insert_user, unique_suffix};
|
||||
|
||||
#[actix_web::test]
|
||||
async fn create_succeeds_for_new_user() {
|
||||
let suffix = unique_suffix();
|
||||
let username = format!("new_user_{suffix}");
|
||||
let email = format!("new_{suffix}@example.test");
|
||||
|
||||
let app = test::init_service(App::new().route("/create", web::post().to(create))).await;
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/create")
|
||||
.set_json(serde_json::json!({
|
||||
"name": username,
|
||||
"email": email,
|
||||
"password": "secret"
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::CREATED, resp.status());
|
||||
|
||||
let mut connection = establish_connection();
|
||||
diesel::delete(users::table.filter(users::username.eq(&username)))
|
||||
.execute(&mut connection)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn create_fails_for_duplicate_user() {
|
||||
let mut connection = establish_connection();
|
||||
let user = insert_user(&mut connection, "secret");
|
||||
|
||||
let app = test::init_service(App::new().route("/create", web::post().to(create))).await;
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/create")
|
||||
.set_json(serde_json::json!({
|
||||
"name": user.username,
|
||||
"email": user.email,
|
||||
"password": "secret"
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(StatusCode::CONFLICT, resp.status());
|
||||
|
||||
delete_user(&mut connection, user.id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user