3 # -----------------------------------------------------------------------------
4 # Unit tests for OpenILS::WWW::EGCatLoader::OpenAthens
5 # -----------------------------------------------------------------------------
7 # These are strict unit tests of this module in isolation. Lower layers are
10 # * The Evergreen context is mocked to provide a dummy base URL etc.
11 # * Apache is mocked to capture redirects being generated
12 # * CGI is mocked to simulate query string input
13 # * HTTP:Request is mocked to capture requests that would be sent to the
15 # * HTTP:Async is mocked to simulate a response from the OpenAthens API
17 # -----------------------------------------------------------------------------
21 use Test::MockObject 0.171;
22 use Test::More tests => 35;
23 use OpenILS::WWW::EGCatLoader;
25 use constant OA_SIGNOUT_URL => qr/https:\/\/login\.openathens\.net\/signout/;
28 use_ok('OpenILS::WWW::EGCatLoader::OpenAthens');
31 # set up an arbitrary global context
34 hostname => 'test.org',
35 opac_root => '/mytesteg/opac',
36 home_page => '/mytesteg/opac/home'
39 # capture output printed to Apache
41 my $apache = Test::MockObject->new()
43 $apache_capture = @_[1];
46 # -----------------------------------------------------------------------------
47 # method under test: perform_openathens_sso_if_required
49 # test case: patron is not logged in
51 # expected outcome: does nothing
52 # -----------------------------------------------------------------------------
54 my $auth_response = {};
55 my $redirect_to = '/mytesteg/opac/home';
56 $apache_capture = undef;
58 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
59 $mut->perform_openathens_sso_if_required($auth_response, $redirect_to);
61 is($apache_capture, undef, 'OpenAthens: no patron: no redirect');
64 # -----------------------------------------------------------------------------
65 # method under test: perform_openathens_sso_if_required
67 # test case: patron is logged in but home OU is not configured for
70 # expected outcome: does nothing
71 # -----------------------------------------------------------------------------
73 my $patron = Test::MockObject->new()
74 ->mock(home_ou => sub { return 123; });
76 my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
77 ->redefine(authtoken => 1)
78 ->redefine(checkauth => 1)
79 ->redefine(requestor => $patron)
80 ->redefine(json_query => [ ]);
82 my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
83 ->redefine(get_org_unit_parent => undef);
85 my $auth_response = { payload => { auth_token => 'abc123' } };
86 my $redirect_to = '/mytesteg/opac/home';
87 $apache_capture = undef;
89 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
90 $mut->perform_openathens_sso_if_required($auth_response, $redirect_to);
92 is($apache_capture, undef, 'OpenAthens: no OA config: no redirect');
95 # -----------------------------------------------------------------------------
96 # method under test: perform_openathens_sso_if_required
98 # test case: patron is logged in and their home OU is configured
99 # to sign in to OpenAthens automatically when logging
102 # expected outcome: issues a redirect to our local OpenAthens sign-on
103 # handler at <OPAC_ROOT>/sso/openathens
104 # -----------------------------------------------------------------------------
106 my $patron = Test::MockObject->new()
107 ->mock(home_ou => sub { return 123; });
111 auto_signon_enabled => 1
114 my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
115 ->redefine(authtoken => 1)
116 ->redefine(checkauth => 1)
117 ->redefine(requestor => $patron)
118 ->redefine(json_query => [ $oa_config ]);
120 my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
121 ->redefine(get_org_unit_parent => undef);
123 my $auth_response = { payload => { auth_token => 'abc123' } };
124 my $redirect_to = '/mytesteg/opac/home';
126 $apache_capture = undef;
127 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
129 $mut->perform_openathens_sso_if_required($auth_response, $redirect_to);
131 my $expected_path = qr/$ctx->{opac_root}\/sso\/openathens/;
132 my $expected_redirect = qr/%2Fmytesteg%2Fopac%2Fhome/;
133 is($result, Apache2::Const::REDIRECT, 'OpenAthens: login: redirects');
134 like($apache_capture, qr/Status: 302/, 'OpenAthens: login: issues 302');
137 qr/Location: ${expected_path}\?redirect_to=${$expected_redirect}/,
138 'OpenAthens: login: correct URL'
142 # -----------------------------------------------------------------------------
143 # method under test: perform_openathens_sso_if_required
145 # test case: login has been initiated from an incoming request via
146 # the OpenAthens handler
148 # expected outcome: does not issue a new redirect, otherwise it would cause
151 my $patron = Test::MockObject->new()
152 ->mock(home_ou => sub { return 123; });
156 auto_signon_enabled => 1
159 my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
160 ->redefine(authtoken => 1)
161 ->redefine(checkauth => 1)
162 ->redefine(requestor => $patron)
163 ->redefine(json_query => [ $oa_config ]);
165 my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
166 ->redefine(get_org_unit_parent => undef);
168 my $auth_response = { payload => { auth_token => 'abc123' } };
169 my $redirect_to = '/mytesteg/opac/sso/openathens?returnData=37580gwev';
171 $apache_capture = undef;
172 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
174 $mut->perform_openathens_sso_if_required($auth_response, $redirect_to);
176 is($apache_capture, undef, 'OpenAthens: login: no redirect loop');
179 # -----------------------------------------------------------------------------
180 # method under test: perform_openathens_signout_if_required
182 # test case: patron is not logged in
184 # expected outcome: does nothing
185 # -----------------------------------------------------------------------------
187 my $redirect_to = '/mytesteg/opac/home';
188 $apache_capture = undef;
190 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
191 $mut->perform_openathens_signout_if_required($redirect_to);
193 is($apache_capture, undef, 'OpenAthens: logout, no patron: no redirect');
196 # -----------------------------------------------------------------------------
197 # method under test: perform_openathens_signout_if_required
199 # test case: patron is logged in but home OU is not configured for
202 # expected outcome: does nothing
203 # -----------------------------------------------------------------------------
205 my $patron = Test::MockObject->new()
206 ->mock(home_ou => sub { return 123; });
208 $ctx->{user} = $patron;
210 my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
211 ->redefine(json_query => [ ]);
213 my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
214 ->redefine(get_org_unit_parent => undef);
216 my $redirect_to = '/mytesteg/opac/home';
217 $apache_capture = undef;
219 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
220 $mut->perform_openathens_signout_if_required($redirect_to);
222 is($apache_capture, undef, 'OpenAthens: logout no OA config: no redirect');
225 # -----------------------------------------------------------------------------
226 # method under test: perform_openathens_signout_if_required
228 # test case: patron is logged in and their home OU is configured
229 # to sign out of OpenAthens automatically when logging
232 # expected outcome: issues a redirect to our local OpenAthens sign-out
233 # handler at <OPAC_ROOT>/sso/openathens/logout
234 # -----------------------------------------------------------------------------
236 my $patron = Test::MockObject->new()
237 ->mock(home_ou => sub { return 123; });
241 auto_signout_enabled => 1
244 $ctx->{user} = $patron;
246 my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
247 ->redefine(json_query => [ $oa_config ]);
249 my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
250 ->redefine(get_org_unit_parent => undef);
252 my $redirect_to = '/mytesteg/opac/home';
254 $apache_capture = undef;
255 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
257 $mut->perform_openathens_signout_if_required($redirect_to);
259 my $expected_path = qr/$ctx->{opac_root}\/sso\/openathens\/logout/;
260 my $expected_redirect = qr/%2Fmytesteg%2Fopac%2Fhome/;
261 is($result, Apache2::Const::REDIRECT, 'OpenAthens: logout: redirects');
262 like($apache_capture, qr/Status: 302/, 'OpenAthens: logout: issues 302');
265 qr/Location: ${expected_path}\?redirect_to=${$expected_redirect}/,
266 'OpenAthens: logout: correct URL'
270 # -----------------------------------------------------------------------------
271 # method under test: load_openathens_sso - for OPAC_HOME/sso/openathens
273 # test case: 1) initiated by Evergreen - ?redirect_to= is present
275 # expected outcome: queries the OpenAthens API to obtain a unique session
276 # creation URL for the logged in patron, then issues a
277 # redirect to that URL
278 # -----------------------------------------------------------------------------
280 my $patron = Test::MockObject->new()
281 ->mock(id => sub { return 42; })
282 ->mock(home_ou => sub { return 123; });
284 my $api_endpoint = 'https://login.openathens.net/api/etc';
287 auto_signon_enabled => 1,
290 connection_uri => $api_endpoint,
291 connection_id => '123456',
295 $ctx->{user} = $patron;
297 my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
298 ->redefine(json_query => [ $oa_config ]);
300 my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
301 ->redefine(get_org_unit_parent => undef);
303 # mock the query string
304 my $redirect_to = '/mytesteg/opac/home';
305 my $cgi = Test::MockModule->new('CGI')
306 ->redefine(param => sub {
308 return $redirect_to if ($key eq 'redirect_to');
312 # the object we expect to be posted to the OpenAthens API
313 my $expected_api_request = {
314 connectionID => '123456',
315 uniqueUserIdentifier => 42,
318 returnUrl => 'https://test.org/mytesteg/opac/sso/openathens'
319 . '?redirect_to=%2Fmytesteg%2Fopac%2Fhome'
322 # create a mock OpenAthens API JSON response
323 my $sso_url = 'https://login.openathens.net/account/sso?t=eyj0e';
324 my $openathens_response_body = "{\"sessionInitiatorUrl\":\"$sso_url\"}";
325 my $openathens_response = Test::MockObject->new()
326 ->mock(is_error => sub { return 0; })
327 ->mock(content => sub { return $openathens_response_body; });
329 # mock the web request to the API
330 my $http_request_capture;
331 my $async = Test::MockModule->new('HTTP::Async')
332 ->redefine(add => sub {
333 # capture the HTTP request that is built, to check later
334 $http_request_capture = @_[1];
336 # mock the async behaviour to return our mocked response
337 # without using the network
338 ->redefine(wait_for_next_response => $openathens_response);
340 $apache_capture = undef;
341 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
342 my $result = $mut->load_openathens_sso();
344 # check the API HTTP request that was built
345 my $method = $http_request_capture->method;
346 my $uri = $http_request_capture->uri;
347 my $auth_header = $http_request_capture->header('Authorization');
348 my $content_type = $http_request_capture->header('Content-type');
349 my $content = JSON::XS->new->utf8->decode($http_request_capture->content);
350 my $expected_content_type
351 = 'application/vnd.eduserv.iam.auth.localAccountSessionRequest+json';
352 is($method, 'POST', 'OpenAthens: SSO 1: uses POST to API');
353 is($uri, $api_endpoint, 'OpenAthens: SSO 1: posts to correct URI');
354 is($auth_header, 'OAApiKey abc123', 'OpenAthens: SSO 1: uses API key');
355 is($content_type, $expected_content_type, 'OpenAthens: SSO 1: type ok');
356 is_deeply($content, $expected_api_request, 'OpenAthens: SSO 1: data ok');
358 # check the resulting redirect
359 my $expected_redirect
360 = qr/https:\/\/login\.openathens\.net\/account\/sso\?t=eyj0e/;
361 is($result, Apache2::Const::REDIRECT, 'OpenAthens: SSO 1: redirects');
362 like($apache_capture, qr/Status: 302/, 'OpenAthens: SSO 1: issues 302');
363 like($apache_capture, $expected_redirect, 'OpenAthens: SSO 1: URL ok');
366 # -----------------------------------------------------------------------------
367 # method under test: load_openathens_sso - for OPAC_HOME/sso/openathens
369 # test case: 2) initiated by OpenAthens - ?returnData= is present
371 # expected outcome: queries the OpenAthens API to obtain a unique session
372 # creation URL for the logged in patron, then issues a
373 # redirect to that URL
374 # -----------------------------------------------------------------------------
376 my $patron = Test::MockObject->new()
377 ->mock(id => sub { return 42; })
378 ->mock(home_ou => sub { return 123; });
380 my $api_endpoint = 'https://login.openathens.net/api/etc';
383 auto_signon_enabled => 1,
386 connection_uri => $api_endpoint,
387 connection_id => '123456',
391 $ctx->{user} = $patron;
393 my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
394 ->redefine(json_query => [ $oa_config ]);
396 my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
397 ->redefine(get_org_unit_parent => undef);
399 # mock the query string
400 my $return_data = 'jk46gubeuvpweb';
401 my $cgi = Test::MockModule->new('CGI')
402 ->redefine(param => sub {
404 return $return_data if ($key eq 'returnData');
408 # the object we expect to be posted to the OpenAthens API
409 my $expected_api_request = {
410 connectionID => '123456',
411 uniqueUserIdentifier => 42,
414 returnData => 'jk46gubeuvpweb'
417 # create a mock OpenAthens API JSON response
418 my $sso_url = 'https://login.openathens.net/account/sso?t=eyj0e';
419 my $openathens_response_body = "{\"sessionInitiatorUrl\":\"$sso_url\"}";
420 my $openathens_response = Test::MockObject->new()
421 ->mock(is_error => sub { return 0; })
422 ->mock(content => sub { return $openathens_response_body; });
424 # mock the web request to the API
425 my $http_request_capture;
426 my $async = Test::MockModule->new('HTTP::Async')
427 ->redefine(add => sub {
428 # capture the HTTP request that is built, to check later
429 $http_request_capture = @_[1];
431 # mock the async behaviour to return our mocked response
432 # without using the network
433 ->redefine(wait_for_next_response => $openathens_response);
435 $apache_capture = undef;
436 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
437 my $result = $mut->load_openathens_sso();
439 # check the API HTTP request that was built
440 my $method = $http_request_capture->method;
441 my $uri = $http_request_capture->uri;
442 my $auth_header = $http_request_capture->header('Authorization');
443 my $content_type = $http_request_capture->header('Content-type');
444 my $content = JSON::XS->new->utf8->decode($http_request_capture->content);
445 my $expected_content_type
446 = 'application/vnd.eduserv.iam.auth.localAccountSessionRequest+json';
447 is($method, 'POST', 'OpenAthens: SSO 2: uses POST to API');
448 is($uri, $api_endpoint, 'OpenAthens: SSO 2: posts to correct URI');
449 is($auth_header, 'OAApiKey abc123', 'OpenAthens: SSO 2: uses API key');
450 is($content_type, $expected_content_type, 'OpenAthens: SSO 2: type ok');
451 is_deeply($content, $expected_api_request, 'OpenAthens: SSO 2: data ok');
453 # check the resulting redirect
454 my $expected_redirect
455 = qr/https:\/\/login\.openathens\.net\/account\/sso\?t=eyj0e/;
456 is($result, Apache2::Const::REDIRECT, 'OpenAthens: SSO 2: redirects');
457 like($apache_capture, qr/Status: 302/, 'OpenAthens: SSO 2: issues 302');
458 like($apache_capture, $expected_redirect, 'OpenAthens: SSO 2: URL ok');
461 # -----------------------------------------------------------------------------
462 # method under test: load_openathens_sso - for OPAC_HOME/sso/openathens
464 # test case: 3) both ?redirect_to and ?returnData= are present
466 # expected outcome: returns 400 status
467 # -----------------------------------------------------------------------------
469 # mock the query string
470 my $redirect_to = '/mytesteg/opac/home';
471 my $return_data = 'jk46gubeuvpweb';
472 my $cgi = Test::MockModule->new('CGI')
473 ->redefine(param => sub {
475 return $redirect_to if ($key eq 'redirect_to');
476 return $return_data if ($key eq 'returnData');
480 $apache_capture = undef;
481 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
482 my $result = $mut->load_openathens_sso();
484 is($result, Apache2::Const::HTTP_BAD_REQUEST, 'OpenAthens: SSO 3: badreq');
487 # -----------------------------------------------------------------------------
488 # method under test: load_openathens_sso - for OPAC_HOME/sso/openathens
490 # test case: 4) neither ?redirect_to or ?returnData= are present
492 # expected outcome: redirects to OPAC home
493 # -----------------------------------------------------------------------------
495 # mock the empty query string
496 my $cgi = Test::MockModule->new('CGI')
497 ->redefine(param => sub {
501 $apache_capture = undef;
502 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
503 my $result = $mut->load_openathens_sso();
505 is($result, Apache2::Const::REDIRECT, 'OpenAthens: SSO 4: redirects');
506 like($apache_capture, qr/Status: 302/, 'OpenAthens: SSO 4: issues 302');
509 qr/Location: \/mytesteg\/opac\/home/,
510 'OpenAthens: SSO 4: redirects to OPAC home'
514 # -----------------------------------------------------------------------------
515 # method under test: load_openathens_logout
517 # expected outcome: Issues a redirect to the OpenAthens sign-out URL.
518 # -----------------------------------------------------------------------------
520 my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
521 my $result = $mut->load_openathens_logout;
523 is($result, Apache2::Const::REDIRECT, 'OpenAthens: logout: redirects');
524 like($apache_capture, qr/Status: 302/, 'OpenAthens: logout: issues 302');
525 like($apache_capture, OA_SIGNOUT_URL, 'OpenAthens: logout: correct URL');