]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/t/25-OpenILS-WWW-EGCatLoader-OpenAthens.t
LP 2061136 follow-up: ng lint --fix
[Evergreen.git] / Open-ILS / src / perlmods / t / 25-OpenILS-WWW-EGCatLoader-OpenAthens.t
1 #!perl -T
2
3 # -----------------------------------------------------------------------------
4 # Unit tests for OpenILS::WWW::EGCatLoader::OpenAthens
5 # -----------------------------------------------------------------------------
6 #
7 # These are strict unit tests of this module in isolation. Lower layers are
8 # mocked:
9 #
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
14 #   OpenAthens API
15 # * HTTP:Async is mocked to simulate a response from the OpenAthens API
16 #
17 # -----------------------------------------------------------------------------
18
19 use strict;
20 use Test::MockModule;
21 use Test::MockObject 0.171;
22 use Test::More tests => 35;
23 use OpenILS::WWW::EGCatLoader;
24
25 use constant OA_SIGNOUT_URL => qr/https:\/\/login\.openathens\.net\/signout/;
26
27 BEGIN {
28         use_ok('OpenILS::WWW::EGCatLoader::OpenAthens');
29 }
30
31 # set up an arbitrary global context
32 my $ctx = {
33     proto => 'https',
34     hostname => 'test.org',
35     opac_root => '/mytesteg/opac',
36     home_page => '/mytesteg/opac/home'
37 };
38
39 # capture output printed to Apache
40 my $apache_capture;
41 my $apache = Test::MockObject->new()
42     ->mock(print => sub {
43         $apache_capture = @_[1];
44     });
45
46 # -----------------------------------------------------------------------------
47 # method under test:    perform_openathens_sso_if_required
48 #
49 # test case:            patron is not logged in
50 #
51 # expected outcome:     does nothing
52 # -----------------------------------------------------------------------------
53 {
54     my $auth_response = {};
55     my $redirect_to = '/mytesteg/opac/home';
56     $apache_capture = undef;
57
58     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
59     $mut->perform_openathens_sso_if_required($auth_response, $redirect_to);
60     
61     is($apache_capture, undef, 'OpenAthens: no patron: no redirect');
62 }
63
64 # -----------------------------------------------------------------------------
65 # method under test:    perform_openathens_sso_if_required
66 #
67 # test case:            patron is logged in but home OU is not configured for
68 #                       OpenAthens
69 #
70 # expected outcome:     does nothing
71 # -----------------------------------------------------------------------------
72 {
73     my $patron = Test::MockObject->new()
74         ->mock(home_ou => sub { return 123; });
75
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 => [ ]);
81
82     my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
83         ->redefine(get_org_unit_parent => undef);
84
85     my $auth_response = { payload => { auth_token => 'abc123' } };
86     my $redirect_to = '/mytesteg/opac/home';
87     $apache_capture = undef;
88
89     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
90     $mut->perform_openathens_sso_if_required($auth_response, $redirect_to);
91
92     is($apache_capture, undef, 'OpenAthens: no OA config: no redirect');
93 }
94
95 # -----------------------------------------------------------------------------
96 # method under test:    perform_openathens_sso_if_required
97 #
98 # test case:            patron is logged in and their home OU is configured
99 #                       to sign in to OpenAthens automatically when logging
100 #                       in to Evergreen
101 #
102 # expected outcome:     issues a redirect to our local OpenAthens sign-on
103 #                       handler at <OPAC_ROOT>/sso/openathens
104 # -----------------------------------------------------------------------------
105 {
106     my $patron = Test::MockObject->new()
107         ->mock(home_ou => sub { return 123; });
108
109     my $oa_config = {
110         active => 1,
111         auto_signon_enabled => 1
112     };
113
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 ]);
119
120     my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
121         ->redefine(get_org_unit_parent => undef);
122
123     my $auth_response = { payload => { auth_token => 'abc123' } };
124     my $redirect_to = '/mytesteg/opac/home';
125
126     $apache_capture = undef;
127     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
128     my $result =
129         $mut->perform_openathens_sso_if_required($auth_response, $redirect_to);
130
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');
135     like(
136         $apache_capture,
137         qr/Location: ${expected_path}\?redirect_to=${$expected_redirect}/,
138         'OpenAthens: login: correct URL'
139     );
140 }
141
142 # -----------------------------------------------------------------------------
143 # method under test:    perform_openathens_sso_if_required
144 #
145 # test case:            login has been initiated from an incoming request via
146 #                       the OpenAthens handler
147 #
148 # expected outcome:     does not issue a new redirect, otherwise it would cause
149 #                       a redirect loop
150 {
151     my $patron = Test::MockObject->new()
152         ->mock(home_ou => sub { return 123; });
153
154     my $oa_config = {
155         active => 1,
156         auto_signon_enabled => 1
157     };
158
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 ]);
164
165     my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
166         ->redefine(get_org_unit_parent => undef);
167
168     my $auth_response = { payload => { auth_token => 'abc123' } };
169     my $redirect_to = '/mytesteg/opac/sso/openathens?returnData=37580gwev';
170
171     $apache_capture = undef;
172     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
173     my $result =
174         $mut->perform_openathens_sso_if_required($auth_response, $redirect_to);
175
176     is($apache_capture, undef, 'OpenAthens: login: no redirect loop');
177 }
178
179 # -----------------------------------------------------------------------------
180 # method under test:    perform_openathens_signout_if_required
181 #
182 # test case:            patron is not logged in
183 #
184 # expected outcome:     does nothing
185 # -----------------------------------------------------------------------------
186 {
187     my $redirect_to = '/mytesteg/opac/home';
188     $apache_capture = undef;
189
190     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
191     $mut->perform_openathens_signout_if_required($redirect_to);
192     
193     is($apache_capture, undef, 'OpenAthens: logout, no patron: no redirect');
194 }
195
196 # -----------------------------------------------------------------------------
197 # method under test:    perform_openathens_signout_if_required
198 #
199 # test case:            patron is logged in but home OU is not configured for
200 #                       OpenAthens
201 #
202 # expected outcome:     does nothing
203 # -----------------------------------------------------------------------------
204 {
205     my $patron = Test::MockObject->new()
206         ->mock(home_ou => sub { return 123; });
207
208     $ctx->{user} = $patron;
209
210     my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
211         ->redefine(json_query => [ ]);
212
213     my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
214         ->redefine(get_org_unit_parent => undef);
215
216     my $redirect_to = '/mytesteg/opac/home';
217     $apache_capture = undef;
218
219     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
220     $mut->perform_openathens_signout_if_required($redirect_to);
221
222     is($apache_capture, undef, 'OpenAthens: logout no OA config: no redirect');
223 }
224
225 # -----------------------------------------------------------------------------
226 # method under test:    perform_openathens_signout_if_required
227 #
228 # test case:            patron is logged in and their home OU is configured
229 #                       to sign out of OpenAthens automatically when logging
230 #                       out of Evergreen
231 #
232 # expected outcome:     issues a redirect to our local OpenAthens sign-out
233 #                       handler at <OPAC_ROOT>/sso/openathens/logout
234 # -----------------------------------------------------------------------------
235 {
236     my $patron = Test::MockObject->new()
237         ->mock(home_ou => sub { return 123; });
238
239     my $oa_config = {
240         active => 1,
241         auto_signout_enabled => 1
242     };
243
244     $ctx->{user} = $patron;
245
246     my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
247         ->redefine(json_query => [ $oa_config ]);
248
249     my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
250         ->redefine(get_org_unit_parent => undef);
251
252     my $redirect_to = '/mytesteg/opac/home';
253
254     $apache_capture = undef;
255     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
256     my $result =
257         $mut->perform_openathens_signout_if_required($redirect_to);
258
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');
263     like(
264         $apache_capture,
265         qr/Location: ${expected_path}\?redirect_to=${$expected_redirect}/,
266         'OpenAthens: logout: correct URL'
267     );
268 }
269
270 # -----------------------------------------------------------------------------
271 # method under test:    load_openathens_sso - for OPAC_HOME/sso/openathens
272 #
273 # test case:            1) initiated by Evergreen - ?redirect_to= is present
274 #
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 # -----------------------------------------------------------------------------
279 {
280     my $patron = Test::MockObject->new()
281         ->mock(id => sub { return 42; })
282         ->mock(home_ou => sub { return 123; });
283
284     my $api_endpoint = 'https://login.openathens.net/api/etc';
285     my $oa_config = {
286         active => 1,
287         auto_signon_enabled => 1,
288         id_field => 'id',
289         dn_field => 'id',
290         connection_uri => $api_endpoint,
291         connection_id => '123456',
292         api_key => 'abc123'
293     };
294
295     $ctx->{user} = $patron;
296
297     my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
298         ->redefine(json_query => [ $oa_config ]);
299
300     my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
301         ->redefine(get_org_unit_parent => undef);
302
303     # mock the query string
304     my $redirect_to = '/mytesteg/opac/home';
305     my $cgi = Test::MockModule->new('CGI')
306         ->redefine(param => sub {
307             my $key = @_[1];
308             return $redirect_to if ($key eq 'redirect_to');
309             return undef;
310         });
311
312     # the object we expect to be posted to the OpenAthens API
313     my $expected_api_request = {
314         connectionID => '123456',
315         uniqueUserIdentifier => 42,
316         displayName => 42,
317         attributes => {},
318         returnUrl => 'https://test.org/mytesteg/opac/sso/openathens'
319             . '?redirect_to=%2Fmytesteg%2Fopac%2Fhome'
320     };
321
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; });
328
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];
335         })
336         # mock the async behaviour to return our mocked response
337         # without using the network
338         ->redefine(wait_for_next_response => $openathens_response);
339
340     $apache_capture = undef;
341     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
342     my $result = $mut->load_openathens_sso();
343
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');
357
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');
364 }
365
366 # -----------------------------------------------------------------------------
367 # method under test:    load_openathens_sso - for OPAC_HOME/sso/openathens
368 #
369 # test case:            2) initiated by OpenAthens - ?returnData= is present
370 #
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 # -----------------------------------------------------------------------------
375 {
376     my $patron = Test::MockObject->new()
377         ->mock(id => sub { return 42; })
378         ->mock(home_ou => sub { return 123; });
379
380     my $api_endpoint = 'https://login.openathens.net/api/etc';
381     my $oa_config = {
382         active => 1,
383         auto_signon_enabled => 1,
384         id_field => 'id',
385         dn_field => 'id',
386         connection_uri => $api_endpoint,
387         connection_id => '123456',
388         api_key => 'abc123'
389     };
390
391     $ctx->{user} = $patron;
392
393     my $editor = Test::MockModule->new('OpenILS::Utils::CStoreEditor')
394         ->redefine(json_query => [ $oa_config ]);
395
396     my $utils = Test::MockModule->new('OpenILS::Application::AppUtils')
397         ->redefine(get_org_unit_parent => undef);
398
399     # mock the query string
400     my $return_data = 'jk46gubeuvpweb';
401     my $cgi = Test::MockModule->new('CGI')
402         ->redefine(param => sub {
403             my $key = @_[1];
404             return $return_data if ($key eq 'returnData');
405             return undef;
406         });
407
408     # the object we expect to be posted to the OpenAthens API
409     my $expected_api_request = {
410         connectionID => '123456',
411         uniqueUserIdentifier => 42,
412         displayName => 42,
413         attributes => {},
414         returnData => 'jk46gubeuvpweb'
415     };
416
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; });
423
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];
430         })
431         # mock the async behaviour to return our mocked response
432         # without using the network
433         ->redefine(wait_for_next_response => $openathens_response);
434
435     $apache_capture = undef;
436     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
437     my $result = $mut->load_openathens_sso();
438
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');
452
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');
459 }
460
461 # -----------------------------------------------------------------------------
462 # method under test:    load_openathens_sso - for OPAC_HOME/sso/openathens
463 #
464 # test case:            3) both ?redirect_to and ?returnData= are present
465 #
466 # expected outcome:     returns 400 status
467 # -----------------------------------------------------------------------------
468 {
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 {
474             my $key = @_[1];
475             return $redirect_to if ($key eq 'redirect_to');
476             return $return_data if ($key eq 'returnData');
477             return undef;
478         });
479
480     $apache_capture = undef;
481     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
482     my $result = $mut->load_openathens_sso();
483
484     is($result, Apache2::Const::HTTP_BAD_REQUEST, 'OpenAthens: SSO 3: badreq');
485 }
486
487 # -----------------------------------------------------------------------------
488 # method under test:    load_openathens_sso - for OPAC_HOME/sso/openathens
489 #
490 # test case:            4) neither ?redirect_to or ?returnData= are present
491 #
492 # expected outcome:     redirects to OPAC home
493 # -----------------------------------------------------------------------------
494 {
495     # mock the empty query string
496     my $cgi = Test::MockModule->new('CGI')
497         ->redefine(param => sub {
498             return undef;
499         });
500
501     $apache_capture = undef;
502     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
503     my $result = $mut->load_openathens_sso();
504
505     is($result, Apache2::Const::REDIRECT, 'OpenAthens: SSO 4: redirects');
506     like($apache_capture, qr/Status: 302/, 'OpenAthens: SSO 4: issues 302');
507     like(
508         $apache_capture,
509         qr/Location: \/mytesteg\/opac\/home/,
510         'OpenAthens: SSO 4: redirects to OPAC home'
511     );
512 }
513
514 # -----------------------------------------------------------------------------
515 # method under test:    load_openathens_logout
516 #
517 # expected outcome:     Issues a redirect to the OpenAthens sign-out URL.
518 # -----------------------------------------------------------------------------
519 {
520     my $mut = OpenILS::WWW::EGCatLoader->new($apache, { %$ctx });
521     my $result = $mut->load_openathens_logout;
522
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');
526 }