Set up a function to update the tabletop image and a test for that
All checks were successful
Monorepo build / build-flake (push) Successful in 3s

This commit is contained in:
Savanni D'Gerinel 2025-07-27 22:50:50 -04:00
parent 7cb38bff77
commit 39b4bda954
6 changed files with 119 additions and 34 deletions

2
.config/nextest.toml Normal file
View File

@ -0,0 +1,2 @@
[profile.default]
slow-timeout = { period = "5s", terminate-after = 2 }

View File

@ -16,9 +16,11 @@ pub async fn set_tabletop_image(
) -> (StatusCode, Json<Option<()>>) {
auth_required(state.clone(), headers, move |user| {
clone!((state, request), async move {
let Json(request) = request;
let state = state.write().await;
(StatusCode::INTERNAL_SERVER_ERROR, ())
let Json(SetTabletopImageRequest { game_id, url }) = request;
match state.write().await.set_tabletop_image(&game_id, url) {
Ok(()) => (StatusCode::OK, ()),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, ()),
}
})
})
.await
@ -28,24 +30,73 @@ pub async fn set_tabletop_image(
mod test {
use std::sync::Arc;
use async_channel::Sender;
use axum::http::StatusCode;
use axum_test::TestServer;
use cool_asserts::assert_matches;
use tokio::sync::RwLock;
use visions_types::{GameId, SetTabletopImageRequest};
use visions_types::{
AuthRequest, AuthResponse, GameId, GameMessage, SessionId, SetTabletopImageRequest,
};
use crate::{router::api_routes, state::AppState};
use crate::{dispatcher::Dispatcher, router::api_routes, state::AppState};
fn test_server() -> TestServer {
let app_state = AppState::new();
let my_app = api_routes(Arc::new(RwLock::new(app_state)));
TestServer::new(my_app).expect("test server to succeed")
fn test_server() -> (TestServer, Dispatcher) {
let app_state = Arc::new(RwLock::new(AppState::new()));
let (dispatcher, _) = Dispatcher::new(app_state.clone());
tokio::spawn({
let dispatcher = dispatcher.clone();
async move {
dispatcher.run().await;
}
});
let my_app = api_routes(app_state);
(
TestServer::new(my_app).expect("test server to succeed"),
dispatcher,
)
}
async fn authenticate(server: &TestServer, username: String, password: String) -> SessionId {
let response = server
.post("/auth")
.json(&AuthRequest { username, password })
.await;
response.assert_status(StatusCode::OK);
let auth: AuthResponse = response.json();
match auth {
AuthResponse::Success((session_id, _)) => return session_id,
AuthResponse::PasswordReset(_) => panic!("Did not expect password reset"),
};
}
#[tokio::test]
async fn gm_can_set_tabletop_image_on_their_game() {
todo!();
let (server, dispatcher) = test_server();
let session_id = authenticate(&server, "vakarian".to_owned(), "aoeu".to_owned()).await;
let (sender, receive_from_dispatch) = async_channel::unbounded();
dispatcher.register(session_id.clone(), "missing-librarians-id".into(), sender);
let response = server
.post("/tabletop/image")
.add_header("AUTHORIZATION", format!("Bearer {}", session_id.as_str()))
.json(&SetTabletopImageRequest {
game_id: "missing-librarians-id".into(),
url: "http://nowhere.com/nothing.png".to_owned(),
})
.await;
response.assert_status(StatusCode::OK);
let response = receive_from_dispatch.recv().await.unwrap();
assert_matches!(response, GameMessage::Tabletop(url) => {
assert_eq!(url, "http://nowhere.com/nothing.png".to_owned())
})
}
/*
#[tokio::test]
async fn gm_cannot_set_tabletop_image_on_another_game() {
todo!();
@ -75,10 +126,12 @@ mod test {
async fn player_in_another_game_does_not_receive_update_on_tabletop_image_change() {
todo!();
}
*/
#[tokio::test]
async fn unauthenticated_sessions_get_rejected() {
let server = test_server();
let (server, dispatcher) = test_server();
let response = server
.post("/tabletop/image")
.json(&SetTabletopImageRequest {
@ -89,8 +142,10 @@ mod test {
response.assert_status(StatusCode::UNAUTHORIZED);
}
/*
#[tokio::test]
async fn invalid_auth_tokens_get_rejected() {
todo!();
}
*/
}

View File

@ -1,24 +1,23 @@
use std::{collections::HashMap, sync::Arc};
use async_channel::Sender;
use axum::{
extract::Path,
extract::{
ws::{Message, Utf8Bytes, WebSocket},
Path, WebSocketUpgrade,
},
http::{
header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap, HeaderValue, Method, StatusCode,
},
routing::{get, post},
routing::{any, get, post},
Json, Router,
};
use tokio::sync::RwLock;
use tower_http::cors::{Any, CorsLayer};
use visions_types::{
AuthRequest, CharacterId, Charsheet, GameId, GameOverview, SetTabletopImageRequest, UserId,
UserOverview,
};
use visions_types::{AuthRequest, CharacterId, GameId, SetTabletopImageRequest};
use crate::{handlers::*, state::AppState, types::User};
use super::{auth_required, check_password};
use crate::{check_password, handlers::*, state::AppState};
pub fn api_routes(state: Arc<RwLock<AppState>>) -> Router {
Router::new()
@ -112,7 +111,9 @@ pub fn api_routes(state: Arc<RwLock<AppState>>) -> Router {
mod test {
use axum::http::StatusCode;
use axum_test::TestServer;
use visions_types::{AccountStatus, AuthResponse, SessionId};
use visions_types::{AccountStatus, AuthResponse, SessionId, UserId, UserOverview};
use crate::dispatcher::Dispatcher;
use super::*;
@ -126,10 +127,14 @@ mod test {
}
}
fn test_server() -> TestServer {
let app_state = AppState::new();
let my_app = api_routes(Arc::new(RwLock::new(app_state)));
TestServer::new(my_app).expect("test server to succeed")
fn test_server() -> (TestServer, Dispatcher) {
let app_state = Arc::new(RwLock::new(AppState::new()));
let (dispatcher, _) = Dispatcher::new(app_state.clone());
let my_app = api_routes(app_state);
(
TestServer::new(my_app).expect("test server to succeed"),
dispatcher,
)
}
async fn authenticate(
@ -165,14 +170,14 @@ mod test {
#[tokio::test]
async fn app_construction() {
let server = test_server();
let (server, _) = test_server();
let response = server.get("/health").await;
response.assert_status(StatusCode::OK);
}
#[tokio::test]
async fn retrieve_all_users() {
let server = test_server();
let (server, _) = test_server();
let response = server.get("/users").await;
response.assert_status(StatusCode::OK);
@ -183,7 +188,7 @@ mod test {
#[tokio::test]
async fn retrieve_self() {
let server = test_server();
let (server, _) = test_server();
let response = server.get("/self").await;
response.assert_status(StatusCode::UNAUTHORIZED);

View File

@ -54,12 +54,13 @@ pub async fn auth_required<B, F, Fut>(
f: F,
) -> (StatusCode, Json<Option<B>>)
where
F: Fn(&User) -> Fut,
F: Fn(User) -> Fut,
Fut: Future<Output = (StatusCode, B)>,
{
match parse_session_header(headers) {
ResultExt::Ok(Some(session_id)) => {
match state.read().await.user_from_session(&session_id) {
let user = state.read().await.user_from_session(&session_id).cloned();
match user {
Some(user) => {
let (code, result) = f(user).await;
(code, Json(Some(result)))

View File

@ -1,9 +1,16 @@
use std::collections::HashMap;
use thiserror::Error;
use visions_types::*;
use crate::types::*;
#[derive(Debug, Error)]
pub enum SetTabletopError {
#[error("Game was not found")]
GameNotFound,
}
fn users() -> Vec<User> {
vec![
User {
@ -476,4 +483,18 @@ impl AppState {
.values()
.filter(|sheet| sheet.game_id == *game_id)
}
pub fn set_tabletop_image(
&mut self,
game_id: &GameId,
image_url: String,
) -> Result<(), SetTabletopError> {
match self.games.get_mut(game_id) {
Some(game) => {
game.tabletop.image = Some(image_url);
Ok(())
}
None => Err(SetTabletopError::GameNotFound),
}
}
}

View File

@ -15,15 +15,16 @@ pub enum GameRequest {
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "content", rename_all = "kebab-case")]
pub enum GameMessage {
Title(String),
Background(String),
AvailableScenes(Vec<(SceneId, String)>),
AvailableImages(Vec<String>),
AvailableScenes(Vec<(SceneId, String)>),
Background(String),
CardSidebar,
CharacterOverviews(Vec<Charsheet>),
Charsheet(Charsheet),
PlaceCard(Card, ScreenPosition),
MoveCard(CardId, ScreenPosition),
PlaceCard(Card, ScreenPosition),
Tabletop(String),
Title(String),
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]