stub out auth endpoints

This commit is contained in:
azdle 2023-12-19 15:36:23 -06:00
parent 6a9f4c87c0
commit a8648702a3
5 changed files with 192 additions and 44 deletions

View 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()
}
}

View file

@ -1,7 +1,12 @@
mod auth;
use axum::{routing::get, 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

72
tests/auth.rs Normal file
View 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
}

View file

@ -1,48 +1,10 @@
use anyhow::{Context, Result};
use futures_util::FutureExt;
use std::net::SocketAddr;
use test_log::test;
use tokio::{sync::oneshot, task::JoinHandle};
use tracing::info;
use zero_to_axum::ZeroToAxum;
pub mod fixture;
use fixture::TestServer;
struct TestServer {
server_task_handle: JoinHandle<()>,
shutdown_handle: oneshot::Sender<()>,
addr: SocketAddr,
}
use anyhow::Result;
use test_log::test as traced;
impl TestServer {
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)]
#[traced(tokio::test)]
async fn health_check() -> Result<()> {
let server = TestServer::spawn().await;
let status = reqwest::get(server.url("/health")).await?.status();

42
tests/fixture/mod.rs Normal file
View 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")
}
}