#!/usr/bin/perl
#
# run-tests - Run mod_webkdc test pages via WWW::Mechanize
#
# Written by Jon Robertson <jonrober@stanford.edu>
# Copyright 2014
#     The Board of Trustees of the Leland Stanford Junior University

#############################################################################
# Modules and declarations
#############################################################################

use 5.006;
use autodie;
use strict;
use warnings;

use lib 'tests/mod_webauth/lib';

use Authen::OATH;
use Crypt::GeneratePassword qw(chars);
use Getopt::Long::Descriptive;
use IO::Handle;
use JSON;
use MIME::Base32;
use Net::Remctl;
use Test::More;
use WWW::Mechanize;

use WebLogin::Tests qw(setup_users teardown_users logout login_success
    nologin);

use Data::Dumper;

# Our option descriptions, for both defining options and their usage.
our @OPTIONS = (
    ['help|h',     'print usage (this text) and exit'],
    ['manual|man', 'print perldoc and exit'],
    ['onlytest=i', 'Run only a specific test'],
);

my $URL_ROOT = 'https://weblogin-test.stanford.edu/tests/';

my %TEST_USERS = (
                  low_multifactor  => {
                      username    => 'wa0low',
                      password    => '',
                      type        => 'HOTP',
                      key         => '',
                      otps        => undef,
                  },
                  high_multifactor => {
                      username    => 'wa0high',
                      password    => '',
                      type        => 'TOTP',
                      key         => '',
                      otps        => undef,
                  },
                 );

#############################################################################
# Main routine
#############################################################################

# Get errors and output in the same order.
STDOUT->autoflush;

# Clean up the path name.
my $fullpath = $0;
$0 =~ s{ ^ .* / }{}xms;

# Parse command-line options.
my ($options, $usage) = describe_options("$0 %o <args>", @OPTIONS);
if ($options->manual) {
    print "Feeding myself to perldoc, please wait....\n";
    exec 'perldoc', '-t', $fullpath;
} elsif ($options->help) {
    print $usage->text;
    exit 0;
}

setup_users(%TEST_USERS);

my $mech = WWW::Mechanize->new;

# Use the high-multifactor test user by default, since that one doesn't have
# a limited list.
my $username = $TEST_USERS{high_multifactor}{username};
my $password = $TEST_USERS{high_multifactor}{password};

my ($url, $finalurl, $match, $mf);

# Test page one only needs to see if we logged in.
if (!$options->onlytest || $options->onlytest == 1) {
    $url  = $URL_ROOT . 'auth/test1';
    $mf   = login_success($mech, $url, 'high_multifactor');
    $mech = logout();
}

# Test page two we also want to make sure redirected to a final URL.
if (!$options->onlytest || $options->onlytest == 2) {
    $url         = $URL_ROOT . 'auth/test2';
    $finalurl = $URL_ROOT . 'auth/test2return';
    $mf   = login_success($mech, $url, 'high_multifactor');
    is($mech->uri, $finalurl, '... and goes to the right end URL');
    $mech = logout();
}

# Page three tries to prefix variables with TEST_ to test WebAuthVarPrefix.
if (!$options->onlytest || $options->onlytest == 3) {
    $url  = $URL_ROOT . 'auth/test3';
    $mf    = login_success($mech, $url, 'high_multifactor');
    $match = qr{<td>TEST_WEBAUTH_USER</td>\s+
        <td>\s+
        <strong>1</strong>\s+
        </td>\s+
        <td>$username</td>}xms;
    like($mech->content, $match, '... and sets the TEST prefix correctly');
    $mech = logout();
}

# Test cancelling rather than logging in, with a WebAuthLoginCanceledURL set.
if (!$options->onlytest || $options->onlytest == 4) {
    $url      = $URL_ROOT . 'auth/test4';
    $finalurl = $URL_ROOT . 'unauth/test4';
    $mech->get($url);
    my $login_form = $mech->form_name('login');
    ok(defined $login_form, "Login form for $url is found");
    $mech->follow_link(text => 'Cancel');
    is($mech->uri, $finalurl,
       '... and canceling the login goes to the correct page');
    $mech = logout();
}

# Page five tries to do another return to a different URL, this time without
# an extra redirect.
if (!$options->onlytest || $options->onlytest == 5) {
    $url      = $URL_ROOT . 'auth/test5';
    $finalurl = $URL_ROOT . 'auth/test5return';
    $mf   = login_success($mech, $url, 'high_multifactor');
    like($mech->uri, qr{^\Q$finalurl\E\b}, '... and goes to the right end URL');
    $mech = logout();
}

# Page six checks on having query parameters preserved after an initial
# redirect.
if (!$options->onlytest || $options->onlytest == 6) {
    $url      = $URL_ROOT . 'auth/test6';
    $finalurl = $URL_ROOT . 'auth/test6return?x=1&y=2';
    $mf   = login_success($mech, $url, 'high_multifactor');
    like($mech->uri, qr{^\Q$finalurl\E\b}, '... and goes to the right end URL');
    $mech = logout();
}

# Test a 5s app token lifetime.  This test does not have WebAuthForceLogin
# set, so you'll actually just go straight back through the WebKDC and to the
# result page.  In a browser you'd want to look to see if you can see the
# page flashing through the webkdc URL, but here you can't do that so much.
# The next test is actually more useful.
# TODO: Error since the second login fails the login screen being shown.
if (!$options->onlytest || $options->onlytest == 7) {
    $url  = $URL_ROOT . 'auth/test7';
    $mf   = login_success($mech, $url, 'high_multifactor');
    sleep 6;
    $mf   = nologin($mech, $url, 'high_multifactor');
    $mech = logout();
}

# Test a 5s app token lifetime with WebAuthForceLogin set.  This will force
# you to log in again after 5s.  Use low_multifactor as high_multifactor is
# TOTP and needs to wait 30s to work.
if (!$options->onlytest || $options->onlytest == 8) {
    $url  = $URL_ROOT . 'auth/test8';
    $mf   = login_success($mech, $url, 'low_multifactor');
    sleep 6;
    $mf   = login_success($mech, $url, 'low_multifactor');
    $mech = logout();
}

# Page nine tests that WEBAUTH_TOKEN_LASTUSED is being updated.  To make it
# work we need to sleep for ten seconds, then hit tha page again.
if (!$options->onlytest || $options->onlytest == 9) {
    $url  = $URL_ROOT . 'auth/test9';
    $mf   = login_success($mech, $url, 'high_multifactor');
    my @links = $mech->links;
    sleep 10;
    $mech->follow_link(url_regex => qr{^/tests/auth/test9\?prev=\d+});
    my ($time1, $time2)
        = ($mech->content =~ m{<p>The current value of WEBAUTH_TOKEN_LASTUSED is .+? \((\d+)\).<br>The previous value of WEBAUTH_TOKEN_LASTUSED was .+? \((\d+)\).</p>});
    ok($time1 > $time2, '... and the last used token was updated');
    $mech = logout();
}

# This will force login if your session is older than 20s.  Log in, reload
# the page after 5s to show that it doesn't require login, then wait 21s and
# login again to show that you do need to re-login this time.
if (!$options->onlytest || $options->onlytest == 10) {
    $url  = $URL_ROOT . 'auth/test10';
    $mf   = login_success($mech, $url, 'high_multifactor');
    sleep 5;
    $mf   = nologin($mech, $url, 'high_multifactor');
    sleep 21;
    $mf   = login_success($mech, $url, 'high_multifactor');
    $mech = logout();
}

# After an initial redirect, a script shouldn't see any query parameters.
if (!$options->onlytest || $options->onlytest == 11) {
    $url      = $URL_ROOT . 'auth/test11';
    $finalurl = $URL_ROOT . 'auth/test11return';
    $mf   = login_success($mech, $url, 'high_multifactor');
    $match = qr{<td>QUERY_STRING</td>\s+
                <td>\s+
                    <strong>1</strong>\s+
                </td>\s+
                <td>set \s to \s nothing</td>}xms;
    like($mech->content, $match, '... and the query string is empty');
    $mech = logout();
}

# Handling of POST with an expired cookie.  The token lifetime is 1s, so hit
# the URL once, then sleep and hit it again.
if (!$options->onlytest || $options->onlytest == 12) {
    $url  = $URL_ROOT . 'auth/test12';
    $mf   = login_success($mech, $url, 'high_multifactor');
    sleep 2;
    my %args = (form_number => 1,
                button      => 'Submit');
    $mech->submit_form(%args);
    like($mech->content,
         qr{If are you seeing this, then you returned from a POST without authentication to this page and everything is ok!},
         '... and testing POST with an expired cookie works');
    $mech = logout();
}

# Test lazy sessions, first by going to the URL and getting no prompting,
# then by logging in to a second URL and being logged in.
if (!$options->onlytest || $options->onlytest == 13) {
    $url  = $URL_ROOT . 'auth/test13';
    $mech->get($url);
    my $login_form = $mech->form_name('login');
    ok(!defined $login_form, "Login for $url as a lazy session is not required");
    like($mech->content,
         qr{You \s+ are \s+ accessing \s+ a \s+ webauth-protected
         \s+ page \s+ as \s+ the \s+ user: \s+ </p>}xms,
         '... and you do not get a username');
    $url  = $URL_ROOT . 'auth/test13login';
    $mf   = login_success($mech, $url, 'high_multifactor');
    $mech = logout();
}

# Test cookie path restrictions.  This sets a limited cookie path, logs in,
# then checks a second URL that doesn't match that path in order to see if
# the cookie is properly limited to the given path.
if (!$options->onlytest || $options->onlytest == 14) {
    $url  = $URL_ROOT . 'path/test14';
    $mf   = login_success($mech, $url, 'high_multifactor');
    $mech->follow_link(url_regex => qr{^/tests/auth/test14unauth});
    like($mech->content,
         qr{Test passed.  You are not authenticated when accessing a different URL.},
         '... and the cookie path limited credential leakage');
    $mech = logout();
}

# Make sure that StanfordAuth still works by setting it and then seeing if we
# see things as we do with WebAuth set.
if (!$options->onlytest || $options->onlytest == 15) {
    $url  = $URL_ROOT . 'auth/test15';
    $mf   = login_success($mech, $url, 'high_multifactor');
    $mech = logout();
}

# TODO: PHP not currently running on the WebKDCs, so this can't be tested
#       there.
# Test that login data is seen normally by PHP.
#$url  = $URL_ROOT . 'php/test1.php';
#$mech = login_success($mech, $url, 'high_multifactor');
#$mech = logout();

teardown_users();
done_testing();

exit 0;

__END__

##############################################################################
# Documentation
##############################################################################

=head1 NAME

run-tests - Run mod_webkdc test pages via WWW::Mechanize

=head1 SYNOPSIS

B<run-tests> [B<-h>] [B<--manual>]

=head1 DESCRIPTION

Run the mod_webkdc multifactor tests.  These require a weblogin server
to already be set up and working, and will try to set up new users with
multifactor configurations in order to do the testing.

These tests rely on Stanford-specific infrastructure for setting up
users and multifactor, and do assume the Stanford templates.  The
WebLogin::Test module would need to be modified for other places.

All tests are currently working save the PHP test, due to us not usually
configuring PHP on the box the tests are run against.

=head1 OPTIONS

=over 4

=item B<-h>, B<--help>

Prints a short command summary for the script.

=item B<--manual>, B<--man>

Prints the perldoc information (this document) for the script.

=item B<--onlytest>=<num>

Only run a specific numbered test rather than running all at once.

=back

=head1 AUTHORS

Jon Robertson <jonrober@stanford.edu>

=cut
