Skip to content

Commit 5fb1f81

Browse files
committed
testapp: Add tests to confirm the configuration of the IdPs
testapp: Add automated selenium testing using the testapp
1 parent 033d2e9 commit 5fb1f81

File tree

3 files changed

+378
-1
lines changed

3 files changed

+378
-1
lines changed

xt/testapp/lib/Saml2Test.pm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use MIME::Base64 qw/ decode_base64 /;
1919
use File::Slurper qw/ read_dir /;
2020
use URN::OASIS::SAML2 qw(:bindings :urn);
2121

22-
our $VERSION = '0.2';
22+
our $VERSION = '0.3';
2323

2424
sub load_config {
2525
my $idp = shift;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use strict;
2+
use warnings;
3+
use Test::More;
4+
5+
use Net::SAML2;
6+
use YAML;
7+
use LWP::UserAgent;
8+
use Carp qw(croak);
9+
use Saml2Test;
10+
11+
use File::Slurper qw/read_text/;
12+
13+
# Check that IdPs directory exists
14+
15+
ok ( -d -e 'IdPs', "'IdPs' directory exists");
16+
17+
my @idps = Saml2Test::load_idps;
18+
19+
foreach my $idp (@idps) {
20+
my $idp_name = $idp->{idp};
21+
#check that metadata exists
22+
ok ( -e "IdPs/$idp_name/metadata.xml", "$idp_name metadata.xml exists" );
23+
24+
# Check that cacert exists
25+
ok ( -e "IdPs/$idp_name/cacert.pem", "$idp_name cacert.pem exists" );
26+
27+
# Load the config for Saml2Test
28+
Saml2Test::load_config($idp_name);
29+
30+
# Load the IdP from the my $idp = Saml2Test::_idp();
31+
my $idp = Saml2Test::_idp();
32+
isa_ok($idp, 'Net::SAML2::IdP');
33+
34+
my $sp = Saml2Test::_sp();
35+
isa_ok($sp, 'Net::SAML2::SP');
36+
37+
# load IdP credentials - this should only be used for usernames
38+
# and passwords that are public or in test systems you control
39+
if ( ! -e "IdPs/$idp_name/credentials.yml" ) {
40+
next;
41+
}
42+
43+
ok ( -e "IdPs/$idp_name/credentials.yml", "Found credentials for $idp_name");
44+
45+
my %params = (
46+
force_authn => 1,
47+
is_passive => 1,
48+
);
49+
50+
# initiate Bindings
51+
foreach my $binding (keys %{$idp->sso_urls}) {
52+
my $authnreq = $sp->authn_request(
53+
$idp->sso_url($binding),
54+
$idp->format || '', # default format.
55+
%params,
56+
)->as_xml;
57+
my $redirect = $sp->sso_redirect_binding($idp, 'SAMLRequest');
58+
isa_ok($redirect, 'Net::SAML2::Binding::Redirect');
59+
}
60+
}
61+
62+
done_testing;

xt/testapp/t/004_selenium.t

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
use strict;
2+
use warnings;
3+
use Test::More;
4+
use Selenium::Chrome;
5+
use Selenium::Waiter qw(wait_until);
6+
use FindBin qw( $RealBin );
7+
use File::Slurper qw/ read_dir /;
8+
use File::Spec::Functions qw/catfile/;
9+
use File::Path qw( make_path );
10+
use YAML;
11+
12+
=head2 Selenium setup
13+
14+
=over
15+
16+
=item * L<Module>
17+
18+
cpanm Selenium::Chrome
19+
20+
=item * L<chromedriver>
21+
22+
Download chromedriver (https://chromedriver.chromium.org/downloads) extract
23+
and copy to ~/perl-Net-SAML2/xt/testapp/t/selenium
24+
25+
export set PATH=$PATH:~/perl-Net-SAML2/xt/testapp/t/selenium; perl t/004_selenium.t
26+
27+
=back
28+
29+
=head2 Configuration
30+
31+
Each IdP is set up in xt/testapp/IdPs/idp_name. This script requires
32+
the existance of a configuration file named selenium.yml.
33+
34+
The following fields are supported. Not all IdPs need the full list
35+
of configuration fields.
36+
37+
=over
38+
39+
=item * L<username>
40+
41+
The username to be entered into the IdP
42+
43+
=item * L<username_fn>
44+
45+
The identifier used to find the username field on the IdP login page
46+
47+
=item * L<username_by>
48+
49+
The method used to find the "username_fn" on the IdP login page. The supported
50+
methods are: class, class_name, css, id, link, link_text, partial_link_text,
51+
tag_name, name, xpath.
52+
53+
=item * L<separate_passwd_page>
54+
55+
Boolean that specifies whether the IdP has implemented the username and password
56+
as separate pages.
57+
58+
=item * L<password>
59+
60+
The password to be entered into the IdP
61+
62+
=item * L<password_fn>
63+
64+
The identifier used to find the password field on the IdP login page
65+
66+
=item * L<password_by>
67+
68+
The method used to find the "password_fn" on the IdP login page. The supported
69+
methods are: class, class_name, css, id, link, link_text, partial_link_text,
70+
tag_name, name, xpath.
71+
72+
=item * L<login_btn>
73+
74+
The identifier used to find the login button on th IdP's login page. It is assumed
75+
that "clicking" the button will submit the username and/or password.
76+
77+
Note that where there are separate username and password pages the same button
78+
identifier is assumed to be the same.
79+
80+
=item * L<login_btn_by>
81+
82+
The method used to find the "login_btn" on the IdP login page. The supported
83+
methods are: class, class_name, css, id, link, link_text, partial_link_text,
84+
tag_name, name, xpath.
85+
86+
=item * L<post_login_page_title>
87+
88+
Some IdPs provide a web page that is displayed after login and this allows the
89+
script to recoginize the page and move past it.
90+
91+
=item * L<post_login_page_btn>
92+
93+
The identifier used to find a button on the IdP's post login page. It is assumed
94+
that "clicking" the button will move on to the NetSAML2 testapp's logged in page.
95+
96+
=item * L<post_login_page_btn_by>
97+
98+
The method used to find the "post_login_page_btn" on the IdP login page. The supported
99+
methods are: class, class_name, css, id, link, link_text, partial_link_text,
100+
tag_name, name, xpath.
101+
102+
=item * L<post_logout_page_title>
103+
104+
Some IdPs provide a web page that is displayed after logout and this allows the
105+
script to recoginize the page and move past it to the Net::SAML2 testapp login page.
106+
107+
=item * L<login_bindings>
108+
109+
Array of login bindings that the IdP supports (supported are: post, redirect).
110+
111+
=item * L<logout_bindings>
112+
113+
Array of logout bindings that the IdP supports (supported are: local, post, redirect).
114+
115+
=item * L<issuer_value>
116+
117+
Value of the issuer to find on the user logged in page for Net::SAML2 testapp to
118+
confirm that the login worked
119+
120+
=back
121+
122+
=cut
123+
if (! -d "$RealBin/selenium" ) {
124+
print "Created directory $RealBin/selenium\n" if (make_path("$RealBin/selenium"));
125+
}
126+
127+
if (! -e -f "$RealBin/selenium/chromedriver") {
128+
BAIL_OUT("Please ensure that the chromedriver binary is in $RealBin/selenium/");
129+
}
130+
131+
my $driver = Selenium::Chrome->new(
132+
binary => "$RealBin/selenium/chromedriver",
133+
accept_ssl_certs => 1,
134+
);
135+
136+
isa_ok($driver, 'Selenium::Chrome');
137+
138+
my $ret = $driver->get('https://netsaml2-testapp.local');
139+
if ( $driver->get_title ne 'Saml2Test' ) {
140+
$driver->quit;
141+
BAIL_OUT("Unable to access https://netsaml2-testapp.local");
142+
}
143+
144+
ok($ret, "Access https://netsaml2-testapp.local");
145+
146+
##############################################
147+
# FIXME: Get this based on the list in IdPs
148+
##############################################
149+
150+
ok ( -d -e 'IdPs', "'IdPs' directory exists");
151+
152+
my @idps = load_idps();
153+
154+
##############################################
155+
# Loop through each of the IdPs being tested
156+
##############################################
157+
foreach my $idp (@idps) {
158+
subtest "Authenticate and logout at each IdP - $idp" => \&auth_to_idp, $idp;
159+
}
160+
161+
$driver->quit;
162+
163+
sub auth_to_idp {
164+
my $idp = shift;
165+
166+
note "==============================================\n";
167+
note "= Beginning test for IdP: $idp\n";
168+
note "==============================================\n";
169+
# Load the IdP configuration file
170+
my $config_file = catfile( 'IdPs', $idp, 'selenium.yml' );
171+
if ( ! -e $config_file ) {
172+
plan skip_all => "$config_file does not exist";
173+
next;
174+
}
175+
ok (defined $config_file, "selenium config file found for $idp");
176+
177+
my $selenium_config = YAML::LoadFile($config_file);
178+
179+
if ( ! $selenium_config ) {
180+
plan skip_all => "Check format of $config_file";
181+
}
182+
ok ($selenium_config, "Loaded selenium config from $config_file");
183+
184+
############################################
185+
# Loop through each of the logout bindings
186+
############################################
187+
foreach my $logout_binding (@{$selenium_config->{"logout_bindings"}}) {
188+
note "Logout: ", $logout_binding, "\n";
189+
190+
############################################
191+
# Loop through each of the login bindings
192+
# for each of the logout bindings in turn
193+
############################################
194+
foreach my $login_binding (@{$selenium_config->{"login_bindings"}}) {
195+
note "----------------------------------------------\n";
196+
note " Login: $login_binding", "\n";
197+
198+
# The Net::SAML2 testapp has the 'id' set for each of
199+
# the redirect links and POST buttons (ex: keycloak_post)
200+
# Find the link or button and "click" on it.
201+
my $login_id = $idp . "_" . $login_binding;
202+
my $link = wait_until{$driver->find_element($login_id, "id")};
203+
ok($link, "Found login element for: $login_id");
204+
next if ( ! $link );
205+
$link->click('left');
206+
207+
# If you click login and immediately get a Saml2Test webpage
208+
# it means the IdP logged you in automatically. The last
209+
# login session was still considered active.
210+
if ($driver->get_title ne 'Saml2Test') {
211+
# Find the username field and enter the username
212+
my $username = wait_until{$driver->find_element(
213+
$selenium_config->{"username_fn"},
214+
(defined $selenium_config->{"username_by"} ?
215+
$selenium_config->{"username_by"} : "id"
216+
))};
217+
ok($username, "Found username field: $selenium_config->{'username_fn'}");
218+
219+
next if ( ! $username && $logout_binding ne 'local');
220+
221+
$username->send_keys($selenium_config->{'username'});
222+
223+
# If the IdP has separate username and password pages
224+
# submit the username first
225+
if ($selenium_config->{"separate_passwd_page"}) {
226+
# Find the login button and click it
227+
my $login = wait_until{$driver->find_element(
228+
$selenium_config->{"login_btn"},
229+
$selenium_config->{"login_btn_by"})};
230+
ok($login, "Found login continue button: $selenium_config->{'login_btn'}");
231+
next if ( ! $login);
232+
233+
$login->click('left');
234+
}
235+
236+
# Find the password field and enter the password
237+
my $password = wait_until{$driver->find_element(
238+
$selenium_config->{"password_fn"},
239+
defined $selenium_config->{"password_by"} ?
240+
$selenium_config->{"password_by"} : "id"
241+
)};
242+
ok($password, "Found password field: $selenium_config->{'password_fn'}");
243+
next if ( ! $password );
244+
245+
$password->send_keys($selenium_config->{'password'});
246+
247+
# Find the login button and click it
248+
my $login = wait_until{$driver->find_element(
249+
$selenium_config->{"login_btn"},
250+
$selenium_config->{"login_btn_by"})};
251+
ok($login, "Found login button: $selenium_config->{'login_btn'}");
252+
next if ( ! $login);
253+
254+
$login->click('left');
255+
256+
# Check for a post login IdP page and click the continue button
257+
if ( defined $selenium_config->{"post_login_page_title"} &&
258+
$driver->get_title eq $selenium_config->{"post_login_page_title"} )
259+
{
260+
note "Found post login page - continuing\n";
261+
my $proceed = wait_until{$driver->find_element(
262+
$selenium_config->{"post_login_page_btn"},
263+
$selenium_config->{"post_login_page_btn_by"})};
264+
ok($proceed, "Found post login proceed button: $selenium_config->{'post_login_page_btn'}");
265+
next if ( ! $proceed );
266+
$proceed->click('left');
267+
}
268+
} else {
269+
# The IdP automatically logged in the user - it considered
270+
# the last session as still active.
271+
note " Automatically logged in!!!\n"
272+
}
273+
note " Login Complete\n";
274+
275+
# Check the value of the issuer if issuer_value is defined in the config
276+
my $value = wait_until{$driver->find_element("issuer", "id")};
277+
if ( defined $selenium_config->{"issuer_value"} ) {
278+
ok ($selenium_config->{"issuer_value"} eq $value->get_text(), "Login confirmed: $selenium_config->{'issuer_value'} found");
279+
}
280+
281+
note " Logout using: $logout_binding", "\n";
282+
# Find the logout link or post button and click it
283+
my $logout = wait_until{$driver->find_element("logout-$logout_binding", "id")};
284+
ok($logout, "Found logout element: logout-$logout_binding");
285+
$logout->click('left');
286+
287+
# Check for post logout page and open https://netsaml2-testapp.local
288+
# so that the next login can proceed
289+
if ( defined $selenium_config->{"post_logout_page_title"} && $driver->get_title eq $selenium_config->{"post_logout_page_title"} ) {
290+
note " Found post logout - open https://netsaml2-testapp.local\n";
291+
$driver->get('https://netsaml2-testapp.local');
292+
if ( $driver->get_title ne 'Saml2Test' ) {
293+
$driver->quit;
294+
BAIL_OUT("Unable to access https://netsaml2-testapp.local");
295+
}
296+
}
297+
}
298+
note "==============================================\n";
299+
}
300+
};
301+
302+
sub load_idps {
303+
if ( ! -x 'IdPs' ) {
304+
return "<html><pre>You must have a xt/testapp/IdPs directory</pre></html>";
305+
}
306+
my @dirs = read_dir('IdPs');
307+
308+
my @idps;
309+
for my $dir (sort @dirs) {
310+
push (@idps, $dir) if ( -d catfile("IdPs", $dir) );
311+
}
312+
313+
return @idps;
314+
}
315+
done_testing;

0 commit comments

Comments
 (0)