@@ -18,6 +18,7 @@ struct Response {
1818}
1919
2020#[ derive( Debug , Serialize ) ]
21+ #[ cfg_attr( test, derive( Deserialize ) ) ]
2122pub struct AccessRequest < ' a > {
2223 token : & ' a str ,
2324 audience : & ' a str ,
@@ -39,6 +40,7 @@ impl<'a> AccessRequest<'a> {
3940}
4041
4142#[ derive( Debug , Serialize ) ]
43+ #[ cfg_attr( test, derive( Deserialize ) ) ]
4244pub struct AdminRequest < ' a > {
4345 token : & ' a str ,
4446 audience : & ' a str ,
@@ -157,3 +159,217 @@ impl From<reqwest::Error> for AuthError {
157159 Self :: ServerError ( value)
158160 }
159161}
162+
163+ #[ cfg( test) ]
164+ mod tests {
165+ use std:: str:: FromStr as _;
166+
167+ use axum:: http:: HeaderValue ;
168+ use axum_extra:: headers:: authorization:: { Bearer , Credentials } ;
169+ use axum_extra:: headers:: Authorization ;
170+ use httpmock:: MockServer ;
171+ use rstest:: rstest;
172+
173+ use super :: { AccessRequest , AdminRequest , InvalidVisit , PolicyCheck , Visit , AUDIENCE } ;
174+ use crate :: cli:: PolicyOptions ;
175+ use crate :: graphql:: auth:: AuthError ;
176+
177+ fn token ( name : & ' static str ) -> Option < Authorization < Bearer > > {
178+ Some ( Authorization (
179+ Bearer :: decode ( & HeaderValue :: from_str ( & format ! ( "Bearer {name}" ) ) . unwrap ( ) ) . unwrap ( ) ,
180+ ) )
181+ }
182+
183+ #[ test]
184+ fn valid_visit ( ) {
185+ let visit = Visit :: from_str ( "cm12345-1" ) . unwrap ( ) ;
186+ assert_eq ! ( visit. session, 1 ) ;
187+ assert_eq ! ( visit. proposal, 12345 ) ;
188+ }
189+
190+ #[ rstest]
191+ #[ case:: no_proposal( "cm-3" ) ]
192+ #[ case:: no_session( "cm12345" ) ]
193+ #[ case:: invalid_session( "cm12345-abc" ) ]
194+ #[ case:: invalid_proposal( "cm123abc-12" ) ]
195+ #[ case:: negative_session( "cm1234--12" ) ]
196+ fn invalid_visit ( #[ case] visit : & str ) {
197+ assert ! ( matches!( Visit :: from_str( visit) , Err ( InvalidVisit ) ) )
198+ }
199+
200+ #[ tokio:: test]
201+ async fn successful_access_check ( ) {
202+ let server = MockServer :: start ( ) ;
203+ let mock = server
204+ . mock_async ( |when, then| {
205+ when. method ( "POST" )
206+ . path ( "/demo/access" )
207+ . json_body_obj ( & AccessRequest {
208+ token : "token" ,
209+ beamline : "i22" ,
210+ visit : 4 ,
211+ proposal : 1234 ,
212+ audience : AUDIENCE ,
213+ } ) ;
214+ then. status ( 200 ) . body ( r#"{"result": true}"# ) ;
215+ } )
216+ . await ;
217+ let check = PolicyCheck :: new ( PolicyOptions {
218+ policy_host : server. url ( "" ) ,
219+ visit_query : "demo/access" . into ( ) ,
220+ admin_query : "demo/admin" . into ( ) ,
221+ } ) ;
222+ check
223+ . check_access ( token ( "token" ) . as_ref ( ) , "i22" , "cm1234-4" )
224+ . await
225+ . unwrap ( ) ;
226+ mock. assert ( ) ;
227+ }
228+
229+ #[ tokio:: test]
230+ async fn successful_admin_check ( ) {
231+ let server = MockServer :: start ( ) ;
232+ let mock = server
233+ . mock_async ( |when, then| {
234+ when. method ( "POST" )
235+ . path ( "/demo/admin" )
236+ . json_body_obj ( & AdminRequest {
237+ token : "token" ,
238+ beamline : "i22" ,
239+ audience : AUDIENCE ,
240+ } ) ;
241+ then. status ( 200 ) . body ( r#"{"result": true}"# ) ;
242+ } )
243+ . await ;
244+ let check = PolicyCheck :: new ( PolicyOptions {
245+ policy_host : server. url ( "" ) ,
246+ visit_query : "demo/access" . into ( ) ,
247+ admin_query : "demo/admin" . into ( ) ,
248+ } ) ;
249+ check
250+ . check_admin ( token ( "token" ) . as_ref ( ) , "i22" )
251+ . await
252+ . unwrap ( ) ;
253+ mock. assert ( ) ;
254+ }
255+
256+ #[ tokio:: test]
257+ async fn denied_access_check ( ) {
258+ let server = MockServer :: start ( ) ;
259+ let mock = server
260+ . mock_async ( |when, then| {
261+ when. method ( "POST" )
262+ . path ( "/demo/access" )
263+ . json_body_obj ( & AccessRequest {
264+ token : "token" ,
265+ beamline : "i22" ,
266+ proposal : 1234 ,
267+ visit : 4 ,
268+ audience : AUDIENCE ,
269+ } ) ;
270+ then. status ( 200 ) . body ( r#"{"result": false}"# ) ;
271+ } )
272+ . await ;
273+ let check = PolicyCheck :: new ( PolicyOptions {
274+ policy_host : server. url ( "" ) ,
275+ visit_query : "demo/access" . into ( ) ,
276+ admin_query : "demo/admin" . into ( ) ,
277+ } ) ;
278+
279+ let result = check
280+ . check_access ( token ( "token" ) . as_ref ( ) , "i22" , "cm1234-4" )
281+ . await ;
282+ let Err ( AuthError :: Failed ) = result else {
283+ panic ! ( "Unexpected result from unauthorised check: {result:?}" ) ;
284+ } ;
285+ mock. assert ( ) ;
286+ }
287+
288+ #[ tokio:: test]
289+ async fn denied_admin_check ( ) {
290+ let server = MockServer :: start ( ) ;
291+ let mock = server
292+ . mock_async ( |when, then| {
293+ when. method ( "POST" )
294+ . path ( "/demo/admin" )
295+ . json_body_obj ( & AdminRequest {
296+ token : "token" ,
297+ beamline : "i22" ,
298+ audience : AUDIENCE ,
299+ } ) ;
300+ then. status ( 200 ) . body ( r#"{"result": false}"# ) ;
301+ } )
302+ . await ;
303+ let check = PolicyCheck :: new ( PolicyOptions {
304+ policy_host : server. url ( "" ) ,
305+ visit_query : "demo/access" . into ( ) ,
306+ admin_query : "demo/admin" . into ( ) ,
307+ } ) ;
308+ let result = check. check_admin ( token ( "token" ) . as_ref ( ) , "i22" ) . await ;
309+ let Err ( AuthError :: Failed ) = result else {
310+ panic ! ( "Unexpected result from unauthorised check: {result:?}" ) ;
311+ } ;
312+ mock. assert ( ) ;
313+ }
314+
315+ #[ tokio:: test]
316+ async fn unauthorised_access_check ( ) {
317+ let server = MockServer :: start ( ) ;
318+ let mock = server
319+ . mock_async ( |_, _| {
320+ // mock that rejects every request
321+ } )
322+ . await ;
323+ let check = PolicyCheck :: new ( PolicyOptions {
324+ policy_host : server. url ( "" ) ,
325+ visit_query : "demo/access" . into ( ) ,
326+ admin_query : "demo/admin" . into ( ) ,
327+ } ) ;
328+ let result = check. check_access ( None , "i22" , "cm1234-4" ) . await ;
329+ let Err ( AuthError :: Missing ) = result else {
330+ panic ! ( "Unexpected result from unauthorised check: {result:?}" ) ;
331+ } ;
332+ mock. assert_hits ( 0 ) ;
333+ }
334+
335+ #[ tokio:: test]
336+ async fn unauthorised_admin_check ( ) {
337+ let server = MockServer :: start ( ) ;
338+ let mock = server
339+ . mock_async ( |_, _| {
340+ // mock that rejects every request
341+ } )
342+ . await ;
343+ let check = PolicyCheck :: new ( PolicyOptions {
344+ policy_host : server. url ( "" ) ,
345+ visit_query : "demo/access" . into ( ) ,
346+ admin_query : "demo/admin" . into ( ) ,
347+ } ) ;
348+ let result = check. check_admin ( None , "i22" ) . await ;
349+ let Err ( AuthError :: Missing ) = result else {
350+ panic ! ( "Unexpected result from unauthorised check: {result:?}" ) ;
351+ } ;
352+ mock. assert_hits ( 0 ) ;
353+ }
354+
355+ #[ tokio:: test]
356+ async fn server_error ( ) {
357+ let server = MockServer :: start ( ) ;
358+ let mock = server
359+ . mock_async ( |when, then| {
360+ when. method ( "POST" ) ;
361+ then. status ( 503 ) ;
362+ } )
363+ . await ;
364+ let check = PolicyCheck :: new ( PolicyOptions {
365+ policy_host : server. url ( "" ) ,
366+ visit_query : "demo/access" . into ( ) ,
367+ admin_query : "demo/admin" . into ( ) ,
368+ } ) ;
369+ let result = check. check_admin ( token ( "token" ) . as_ref ( ) , "i22" ) . await ;
370+ let Err ( AuthError :: ServerError ( _) ) = result else {
371+ panic ! ( "Unexpected result from unauthorised check: {result:?}" ) ;
372+ } ;
373+ mock. assert ( ) ;
374+ }
375+ }
0 commit comments