stub out auth endpoints
This commit is contained in:
parent
6a9f4c87c0
commit
a8648702a3
5 changed files with 192 additions and 44 deletions
67
src/server/routes/auth/mod.rs
Normal file
67
src/server/routes/auth/mod.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use axum::{http::StatusCode, response::IntoResponse, routing::post, Form, Router};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
pub fn build() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/login", post(login))
|
||||||
|
.route("/logout", post(logout))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct LoginForm {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(Form(form): Form<LoginForm>) -> Result<(), LoginError> {
|
||||||
|
info!(form.username, form.password, "login attempt");
|
||||||
|
|
||||||
|
if form.username != "admin" {
|
||||||
|
return Err(LoginError::UnknownUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.password != "hunter2" {
|
||||||
|
return Err(LoginError::InvalidPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum LoginError {
|
||||||
|
UnknownUser,
|
||||||
|
InvalidPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for LoginError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
LoginError::UnknownUser => (StatusCode::UNAUTHORIZED, "Unknown User"),
|
||||||
|
LoginError::InvalidPassword => (StatusCode::UNAUTHORIZED, "Invalid Password"),
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout() -> Result<(), LogoutError> {
|
||||||
|
info!("logout attempt");
|
||||||
|
|
||||||
|
if true {
|
||||||
|
return Err(LogoutError::NotLoggedIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum LogoutError {
|
||||||
|
NotLoggedIn,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for LogoutError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
LogoutError::NotLoggedIn => (StatusCode::UNAUTHORIZED, "Unknown User"),
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,12 @@
|
||||||
|
mod auth;
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
|
|
||||||
pub fn build() -> Router {
|
pub fn build() -> Router {
|
||||||
Router::new().route("/health", get(health_check))
|
let auth = auth::build();
|
||||||
|
Router::new()
|
||||||
|
.route("/health", get(health_check))
|
||||||
|
.nest("/auth", auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
// just always returns a 200 OK for now, the server has no state, if it's up, it's working
|
// just always returns a 200 OK for now, the server has no state, if it's up, it's working
|
||||||
|
|
72
tests/auth.rs
Normal file
72
tests/auth.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
pub mod fixture;
|
||||||
|
use fixture::TestServer;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use test_log::test as traced;
|
||||||
|
|
||||||
|
#[traced(tokio::test)]
|
||||||
|
async fn login_succeeds_with_valid_credentials() -> Result<()> {
|
||||||
|
let server = TestServer::spawn().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let resp = client
|
||||||
|
.post(server.url("/auth/login"))
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.body("username=admin&password=hunter2")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(resp.status(), 200, "health check failed");
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
//assert!(resp.headers().get("Set-Cookie").is_some(), "cookie set");
|
||||||
|
|
||||||
|
server.shutdown().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced(tokio::test)]
|
||||||
|
async fn login_fails_with_invalid_credentials() -> Result<()> {
|
||||||
|
let server = TestServer::spawn().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let resp = client
|
||||||
|
.post(server.url("/auth/login"))
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.body("username=admin&password=hunter3")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_ne!(
|
||||||
|
resp.status(),
|
||||||
|
200,
|
||||||
|
"login suceeded with invalid credentials"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
resp.headers().get("Set-Cookie").is_none(),
|
||||||
|
"auth cookie was set for invalid crednetials"
|
||||||
|
);
|
||||||
|
|
||||||
|
server.shutdown().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[traced(tokio::test)]
|
||||||
|
async fn login_rejects_missing_credentials() -> Result<()> {
|
||||||
|
let server = TestServer::spawn().await;
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let resp = client
|
||||||
|
.post(server.url("/auth/login"))
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.body("username=&password=")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.status(),
|
||||||
|
401,
|
||||||
|
"login didn't reject missing credentials"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
resp.headers().get("Set-Cookie").is_none(),
|
||||||
|
"auth cookie was set for missing crednetials"
|
||||||
|
);
|
||||||
|
|
||||||
|
server.shutdown().await
|
||||||
|
}
|
|
@ -1,48 +1,10 @@
|
||||||
use anyhow::{Context, Result};
|
pub mod fixture;
|
||||||
use futures_util::FutureExt;
|
use fixture::TestServer;
|
||||||
use std::net::SocketAddr;
|
|
||||||
use test_log::test;
|
|
||||||
use tokio::{sync::oneshot, task::JoinHandle};
|
|
||||||
use tracing::info;
|
|
||||||
use zero_to_axum::ZeroToAxum;
|
|
||||||
|
|
||||||
struct TestServer {
|
use anyhow::Result;
|
||||||
server_task_handle: JoinHandle<()>,
|
use test_log::test as traced;
|
||||||
shutdown_handle: oneshot::Sender<()>,
|
|
||||||
addr: SocketAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestServer {
|
#[traced(tokio::test)]
|
||||||
async fn spawn() -> TestServer {
|
|
||||||
info!("start server");
|
|
||||||
let mut server = ZeroToAxum::serve("[::]:0".parse().unwrap());
|
|
||||||
let shutdown_handle = server.take_shutdown_handle().unwrap();
|
|
||||||
let addr = server.local_addr();
|
|
||||||
let server_task_handle = tokio::spawn(server.map(|res| res.unwrap()));
|
|
||||||
info!("server spawned");
|
|
||||||
|
|
||||||
TestServer {
|
|
||||||
server_task_handle,
|
|
||||||
shutdown_handle,
|
|
||||||
addr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// format a URL for the given path
|
|
||||||
fn url(&self, path: &str) -> String {
|
|
||||||
format!("http://{}{path}", self.addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request a graceful shutdown and then wait for shutdown to complete
|
|
||||||
async fn shutdown(self) -> Result<()> {
|
|
||||||
self.shutdown_handle.send(()).ok();
|
|
||||||
self.server_task_handle
|
|
||||||
.await
|
|
||||||
.context("wait for server shutdown")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test(tokio::test)]
|
|
||||||
async fn health_check() -> Result<()> {
|
async fn health_check() -> Result<()> {
|
||||||
let server = TestServer::spawn().await;
|
let server = TestServer::spawn().await;
|
||||||
let status = reqwest::get(server.url("/health")).await?.status();
|
let status = reqwest::get(server.url("/health")).await?.status();
|
||||||
|
|
42
tests/fixture/mod.rs
Normal file
42
tests/fixture/mod.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use futures_util::FutureExt;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use tokio::{sync::oneshot, task::JoinHandle};
|
||||||
|
use tracing::info;
|
||||||
|
use zero_to_axum::ZeroToAxum;
|
||||||
|
|
||||||
|
pub struct TestServer {
|
||||||
|
server_task_handle: JoinHandle<()>,
|
||||||
|
shutdown_handle: oneshot::Sender<()>,
|
||||||
|
addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestServer {
|
||||||
|
pub async fn spawn() -> TestServer {
|
||||||
|
info!("start server");
|
||||||
|
let mut server = ZeroToAxum::serve("[::]:0".parse().unwrap());
|
||||||
|
let shutdown_handle = server.take_shutdown_handle().unwrap();
|
||||||
|
let addr = server.local_addr();
|
||||||
|
let server_task_handle = tokio::spawn(server.map(|res| res.unwrap()));
|
||||||
|
info!("server spawned");
|
||||||
|
|
||||||
|
TestServer {
|
||||||
|
server_task_handle,
|
||||||
|
shutdown_handle,
|
||||||
|
addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// format a URL for the given path
|
||||||
|
pub fn url(&self, path: &str) -> String {
|
||||||
|
format!("http://{}{path}", self.addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request a graceful shutdown and then wait for shutdown to complete
|
||||||
|
pub async fn shutdown(self) -> Result<()> {
|
||||||
|
self.shutdown_handle.send(()).ok();
|
||||||
|
self.server_task_handle
|
||||||
|
.await
|
||||||
|
.context("wait for server shutdown")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue