|
| 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