mirror of
https://github.com/actix/actix-extras.git
synced 2025-06-26 10:27:42 +02:00
Rebuild actix-identity
on top of actix-session
(#246)
Co-authored-by: Rob Ede <robjtede@icloud.com>
This commit is contained in:
17
actix-identity/tests/integration/fixtures.rs
Normal file
17
actix-identity/tests/integration/fixtures.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
|
||||
use actix_web::cookie::Key;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn store() -> CookieSessionStore {
|
||||
CookieSessionStore::default()
|
||||
}
|
||||
|
||||
pub fn user_id() -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
pub fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
|
||||
SessionMiddleware::builder(store(), Key::generate())
|
||||
.cookie_domain(Some("localhost".into()))
|
||||
.build()
|
||||
}
|
168
actix-identity/tests/integration/integration.rs
Normal file
168
actix-identity/tests/integration/integration.rs
Normal file
@ -0,0 +1,168 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_identity::{config::LogoutBehaviour, IdentityMiddleware};
|
||||
use actix_web::http::StatusCode;
|
||||
|
||||
use crate::{fixtures::user_id, test_app::TestApp};
|
||||
|
||||
#[actix_web::test]
|
||||
async fn opaque_401_is_returned_for_unauthenticated_users() {
|
||||
let app = TestApp::spawn();
|
||||
|
||||
let response = app.get_identity_required().await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
assert!(response.bytes().await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn login_works() {
|
||||
let app = TestApp::spawn();
|
||||
let user_id = user_id();
|
||||
|
||||
// Log-in
|
||||
let body = app.post_login(user_id.clone()).await;
|
||||
assert_eq!(body.user_id, Some(user_id.clone()));
|
||||
|
||||
// Access identity-restricted route successfully
|
||||
let response = app.get_identity_required().await;
|
||||
assert!(response.status().is_success());
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn logging_in_again_replaces_the_current_identity() {
|
||||
let app = TestApp::spawn();
|
||||
let first_user_id = user_id();
|
||||
let second_user_id = user_id();
|
||||
|
||||
// Log-in
|
||||
let body = app.post_login(first_user_id.clone()).await;
|
||||
assert_eq!(body.user_id, Some(first_user_id.clone()));
|
||||
|
||||
// Log-in again
|
||||
let body = app.post_login(second_user_id.clone()).await;
|
||||
assert_eq!(body.user_id, Some(second_user_id.clone()));
|
||||
|
||||
let body = app.get_current().await;
|
||||
assert_eq!(body.user_id, Some(second_user_id.clone()));
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn session_key_is_renewed_on_login() {
|
||||
let app = TestApp::spawn();
|
||||
let user_id = user_id();
|
||||
|
||||
// Create an anonymous session
|
||||
let body = app.post_increment().await;
|
||||
assert_eq!(body.user_id, None);
|
||||
assert_eq!(body.counter, 1);
|
||||
assert_eq!(body.session_status, "changed");
|
||||
|
||||
// Log-in
|
||||
let body = app.post_login(user_id.clone()).await;
|
||||
assert_eq!(body.user_id, Some(user_id.clone()));
|
||||
assert_eq!(body.counter, 1);
|
||||
assert_eq!(body.session_status, "renewed");
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn logout_works() {
|
||||
let app = TestApp::spawn();
|
||||
let user_id = user_id();
|
||||
|
||||
// Log-in
|
||||
let body = app.post_login(user_id.clone()).await;
|
||||
assert_eq!(body.user_id, Some(user_id.clone()));
|
||||
|
||||
// Log-out
|
||||
let response = app.post_logout().await;
|
||||
assert!(response.status().is_success());
|
||||
|
||||
// Try to access identity-restricted route
|
||||
let response = app.get_identity_required().await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn logout_can_avoid_destroying_the_whole_session() {
|
||||
let app = TestApp::spawn_with_config(
|
||||
IdentityMiddleware::builder().logout_behaviour(LogoutBehaviour::DeleteIdentityKeys),
|
||||
);
|
||||
let user_id = user_id();
|
||||
|
||||
// Log-in
|
||||
let body = app.post_login(user_id.clone()).await;
|
||||
assert_eq!(body.user_id, Some(user_id.clone()));
|
||||
assert_eq!(body.counter, 0);
|
||||
|
||||
// Increment counter
|
||||
let body = app.post_increment().await;
|
||||
assert_eq!(body.user_id, Some(user_id.clone()));
|
||||
assert_eq!(body.counter, 1);
|
||||
|
||||
// Log-out
|
||||
let response = app.post_logout().await;
|
||||
assert!(response.status().is_success());
|
||||
|
||||
// Check the state of the counter attached to the session state
|
||||
let body = app.get_current().await;
|
||||
assert_eq!(body.user_id, None);
|
||||
// It would be 0 if the session state had been entirely lost!
|
||||
assert_eq!(body.counter, 1);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn user_is_logged_out_when_login_deadline_is_elapsed() {
|
||||
let login_deadline = Duration::from_millis(10);
|
||||
let app = TestApp::spawn_with_config(
|
||||
IdentityMiddleware::builder().login_deadline(Some(login_deadline)),
|
||||
);
|
||||
let user_id = user_id();
|
||||
|
||||
// Log-in
|
||||
let body = app.post_login(user_id.clone()).await;
|
||||
assert_eq!(body.user_id, Some(user_id.clone()));
|
||||
|
||||
// Wait for deadline to pass
|
||||
actix_web::rt::time::sleep(login_deadline * 2).await;
|
||||
|
||||
let body = app.get_current().await;
|
||||
// We have been logged out!
|
||||
assert_eq!(body.user_id, None);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn login_deadline_does_not_log_users_out_before_their_time() {
|
||||
// 1 hour
|
||||
let login_deadline = Duration::from_secs(60 * 60);
|
||||
let app = TestApp::spawn_with_config(
|
||||
IdentityMiddleware::builder().login_deadline(Some(login_deadline)),
|
||||
);
|
||||
let user_id = user_id();
|
||||
|
||||
// Log-in
|
||||
let body = app.post_login(user_id.clone()).await;
|
||||
assert_eq!(body.user_id, Some(user_id.clone()));
|
||||
|
||||
let body = app.get_current().await;
|
||||
assert_eq!(body.user_id, Some(user_id));
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn user_is_logged_out_when_visit_deadline_is_elapsed() {
|
||||
let visit_deadline = Duration::from_millis(10);
|
||||
let app = TestApp::spawn_with_config(
|
||||
IdentityMiddleware::builder().visit_deadline(Some(visit_deadline)),
|
||||
);
|
||||
let user_id = user_id();
|
||||
|
||||
// Log-in
|
||||
let body = app.post_login(user_id.clone()).await;
|
||||
assert_eq!(body.user_id, Some(user_id.clone()));
|
||||
|
||||
// Wait for deadline to pass
|
||||
actix_web::rt::time::sleep(visit_deadline * 2).await;
|
||||
|
||||
let body = app.get_current().await;
|
||||
// We have been logged out!
|
||||
assert_eq!(body.user_id, None);
|
||||
}
|
3
actix-identity/tests/integration/main.rs
Normal file
3
actix-identity/tests/integration/main.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod fixtures;
|
||||
mod integration;
|
||||
pub mod test_app;
|
186
actix-identity/tests/integration/test_app.rs
Normal file
186
actix-identity/tests/integration/test_app.rs
Normal file
@ -0,0 +1,186 @@
|
||||
use std::net::TcpListener;
|
||||
|
||||
use actix_identity::{config::IdentityMiddlewareBuilder, Identity, IdentityMiddleware};
|
||||
use actix_session::{Session, SessionStatus};
|
||||
use actix_web::{web, App, HttpMessage, HttpRequest, HttpResponse, HttpServer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::fixtures::session_middleware;
|
||||
|
||||
pub struct TestApp {
|
||||
port: u16,
|
||||
api_client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl TestApp {
|
||||
/// Spawn a test application using a custom configuration for `IdentityMiddleware`.
|
||||
pub fn spawn_with_config(builder: IdentityMiddlewareBuilder) -> Self {
|
||||
// Random OS port
|
||||
let listener = TcpListener::bind("localhost:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(builder.clone().build())
|
||||
.wrap(session_middleware())
|
||||
.route("/increment", web::post().to(increment))
|
||||
.route("/current", web::get().to(show))
|
||||
.route("/login", web::post().to(login))
|
||||
.route("/logout", web::post().to(logout))
|
||||
.route("/identity_required", web::get().to(identity_required))
|
||||
})
|
||||
.workers(1)
|
||||
.listen(listener)
|
||||
.unwrap()
|
||||
.run();
|
||||
let _ = actix_web::rt::spawn(server);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.cookie_store(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
TestApp {
|
||||
port,
|
||||
api_client: client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a test application using the default configuration settings for `IdentityMiddleware`.
|
||||
pub fn spawn() -> Self {
|
||||
Self::spawn_with_config(IdentityMiddleware::builder())
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
format!("http://localhost:{}", self.port)
|
||||
}
|
||||
|
||||
pub async fn get_identity_required(&self) -> reqwest::Response {
|
||||
self.api_client
|
||||
.get(format!("{}/identity_required", &self.url()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_current(&self) -> EndpointResponse {
|
||||
self.api_client
|
||||
.get(format!("{}/current", &self.url()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn post_increment(&self) -> EndpointResponse {
|
||||
let response = self
|
||||
.api_client
|
||||
.post(format!("{}/increment", &self.url()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
response.json().await.unwrap()
|
||||
}
|
||||
|
||||
pub async fn post_login(&self, user_id: String) -> EndpointResponse {
|
||||
let response = self
|
||||
.api_client
|
||||
.post(format!("{}/login", &self.url()))
|
||||
.json(&LoginRequest { user_id })
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
response.json().await.unwrap()
|
||||
}
|
||||
|
||||
pub async fn post_logout(&self) -> reqwest::Response {
|
||||
self.api_client
|
||||
.post(format!("{}/logout", &self.url()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct EndpointResponse {
|
||||
pub user_id: Option<String>,
|
||||
pub counter: i32,
|
||||
pub session_status: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct LoginRequest {
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
async fn show(user: Option<Identity>, session: Session) -> HttpResponse {
|
||||
let user_id = user.map(|u| u.id().unwrap());
|
||||
let counter: i32 = session
|
||||
.get::<i32>("counter")
|
||||
.unwrap_or(Some(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
HttpResponse::Ok().json(&EndpointResponse {
|
||||
user_id,
|
||||
counter,
|
||||
session_status: session_status(session),
|
||||
})
|
||||
}
|
||||
|
||||
async fn increment(session: Session, user: Option<Identity>) -> HttpResponse {
|
||||
let user_id = user.map(|u| u.id().unwrap());
|
||||
let counter: i32 = session
|
||||
.get::<i32>("counter")
|
||||
.unwrap_or(Some(0))
|
||||
.map_or(1, |inner| inner + 1);
|
||||
session.insert("counter", &counter).unwrap();
|
||||
|
||||
HttpResponse::Ok().json(&EndpointResponse {
|
||||
user_id,
|
||||
counter,
|
||||
session_status: session_status(session),
|
||||
})
|
||||
}
|
||||
|
||||
async fn login(
|
||||
user_id: web::Json<LoginRequest>,
|
||||
request: HttpRequest,
|
||||
session: Session,
|
||||
) -> HttpResponse {
|
||||
let id = user_id.into_inner().user_id;
|
||||
let user = Identity::login(&request.extensions(), id).unwrap();
|
||||
|
||||
let counter: i32 = session
|
||||
.get::<i32>("counter")
|
||||
.unwrap_or(Some(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
HttpResponse::Ok().json(&EndpointResponse {
|
||||
user_id: Some(user.id().unwrap()),
|
||||
counter,
|
||||
session_status: session_status(session),
|
||||
})
|
||||
}
|
||||
|
||||
async fn logout(user: Option<Identity>) -> HttpResponse {
|
||||
if let Some(user) = user {
|
||||
user.logout();
|
||||
}
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
async fn identity_required(_identity: Identity) -> HttpResponse {
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
fn session_status(session: Session) -> String {
|
||||
match session.status() {
|
||||
SessionStatus::Changed => "changed",
|
||||
SessionStatus::Purged => "purged",
|
||||
SessionStatus::Renewed => "renewed",
|
||||
SessionStatus::Unchanged => "unchanged",
|
||||
}
|
||||
.into()
|
||||
}
|
Reference in New Issue
Block a user