@@ -19,10 +19,8 @@ pub trait SessionInfoProvider: Clone + Send + Sync {
1919
2020/// Build a router for the dashboard UI
2121///
22- /// This returns a router that works with any state `S` where you can
23- /// extract a `P: SessionInfoProvider` using `axum::extract::FromRef`.
24- ///
25- /// Typically, your state will be a struct with a `controller` field.
22+ /// This requires router state that can serve the required data
23+ /// to the endpoints as defined in [`SessionInfoProvider`].
2624pub fn build_ui_router < S , P > ( ) -> Router < S >
2725where
2826 S : Clone + Send + Sync + ' static ,
@@ -33,3 +31,102 @@ where
3331 . route ( "/" , get ( dashboard:: dashboard_handler :: < S , P > ) )
3432 . route ( "/static/{*file}" , get ( assets:: static_handler) )
3533}
34+
35+ #[ cfg( test) ]
36+ mod tests {
37+ use super :: * ;
38+ use axum:: body:: Body ;
39+ use axum:: http:: { Request , StatusCode } ;
40+ use hotfix:: session:: { SessionInfo , Status } ;
41+ use tower:: ServiceExt ;
42+
43+ #[ derive( Clone ) ]
44+ struct MockSessionProvider {
45+ session_info : SessionInfo ,
46+ }
47+
48+ #[ async_trait:: async_trait]
49+ impl SessionInfoProvider for MockSessionProvider {
50+ async fn get_session_info ( & self ) -> anyhow:: Result < SessionInfo > {
51+ Ok ( self . session_info . clone ( ) )
52+ }
53+ }
54+
55+ fn create_test_app ( ) -> Router {
56+ let provider = MockSessionProvider {
57+ session_info : SessionInfo {
58+ next_sender_seq_number : 42 ,
59+ next_target_seq_number : 100 ,
60+ status : Status :: Active ,
61+ } ,
62+ } ;
63+ build_ui_router :: < MockSessionProvider , MockSessionProvider > ( ) . with_state ( provider)
64+ }
65+
66+ #[ tokio:: test]
67+ async fn test_dashboard_returns_html ( ) {
68+ let app = create_test_app ( ) ;
69+
70+ let response = app
71+ . oneshot ( Request :: builder ( ) . uri ( "/" ) . body ( Body :: empty ( ) ) . unwrap ( ) )
72+ . await
73+ . unwrap ( ) ;
74+
75+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
76+
77+ let content_type = response
78+ . headers ( )
79+ . get ( "content-type" )
80+ . and_then ( |v| v. to_str ( ) . ok ( ) ) ;
81+ assert ! (
82+ content_type. is_some_and( |ct| ct. contains( "text/html" ) ) ,
83+ "Expected HTML content type"
84+ ) ;
85+ }
86+
87+ #[ tokio:: test]
88+ async fn test_static_asset_returns_file ( ) {
89+ let app = create_test_app ( ) ;
90+
91+ let response = app
92+ . oneshot (
93+ Request :: builder ( )
94+ . uri ( "/static/tailwind.js" )
95+ . body ( Body :: empty ( ) )
96+ . unwrap ( ) ,
97+ )
98+ . await
99+ . unwrap ( ) ;
100+
101+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
102+
103+ let content_type = response
104+ . headers ( )
105+ . get ( "content-type" )
106+ . and_then ( |v| v. to_str ( ) . ok ( ) ) ;
107+ assert ! (
108+ content_type. is_some_and(
109+ |ct| ct. contains( "javascript" ) || ct. contains( "application/x-javascript" )
110+ ) ,
111+ "Expected JavaScript content type, got: {:?}" ,
112+ content_type
113+ ) ;
114+ }
115+
116+ #[ tokio:: test]
117+ async fn test_static_asset_not_found ( ) {
118+ let app = create_test_app ( ) ;
119+
120+ let response = app
121+ . oneshot (
122+ Request :: builder ( )
123+ . uri ( "/static/nonexistent.js" )
124+ . body ( Body :: empty ( ) )
125+ . unwrap ( ) ,
126+ )
127+ . await
128+ . unwrap ( ) ;
129+
130+ assert_eq ! ( response. status( ) , StatusCode :: NOT_FOUND ) ;
131+ }
132+ }
0 commit comments