package Test; no autovivification; use strict; use warnings; use CGI; #use Data::Session; # The caller did use_ok on Data::Session. use DBI; use DBIx::Admin::CreateTable; use File::Basename; use File::Spec; use Hash::FieldHash ':all'; use Test::More; fieldhash my %cache => 'cache'; fieldhash my %column_type => 'column_type'; fieldhash my %creator => 'creator'; fieldhash my %dbh => 'dbh'; fieldhash my %directory => 'directory'; fieldhash my %dsn => 'dsn'; fieldhash my %dsn_attr => 'dsn_attr'; fieldhash my %engine => 'engine'; fieldhash my %id => 'id'; fieldhash my %id_base => 'id_base'; fieldhash my %id_file => 'id_file'; fieldhash my %id_step => 'id_step'; fieldhash my %key => 'key'; fieldhash my %type => 'type'; fieldhash my %password => 'password'; fieldhash my %table_name => 'table_name'; fieldhash my %test_count => 'test_count'; fieldhash my %username => 'username'; fieldhash my %value => 'value'; fieldhash my %verbose => 'verbose'; our $errstr = ''; our $VERSION = '1.18'; # ----------------------------------------------- sub check_sqlite_directory_exists { my($self) = @_; my(@dsn) = DBI -> parse_dsn($self -> dsn); my($result) = 1; # Success. if ($dsn[4] && ($dsn[1] =~ /^SQLite/i) ) { my($file, $dir, $suffix) = fileparse($dsn[4]); $result = 0 if (! -e $dir); } return $result; } # End of check_sqlite_directory_exists. # ----------------------------------------------- sub create_session_from_id { my($self, $id) = @_; return Data::Session -> new ( cache => $self -> cache, data_source => $self -> dsn, data_source_attr => $self -> dsn_attr, directory => $self -> directory, id => $id, id_base => $self -> id_base, id_file => $self -> id_file, id_step => $self -> id_step, password => $self -> password, type => $self -> type, username => $self -> username, verbose => $self -> verbose, ) || die __PACKAGE__ . ". $Data::Session::errstr"; } # End of create_session_from_id. # ----------------------------------------------- sub create_session_from_q { my($self, $session1) = @_; my($q) = CGI -> new; $q -> param(sid => $session1 -> id); $q -> param($self -> key => $self -> value); return Data::Session -> new ( cache => $self -> cache, data_source => $self -> dsn, data_source_attr => $self -> dsn_attr, directory => $self -> directory, id => $session1 -> id, id_base => $self -> id_base, id_file => $self -> id_file, id_step => $self -> id_step, name => 'sid', password => $self -> password, query => $q, type => $self -> type, username => $self -> username, verbose => $self -> verbose, ) || die __PACKAGE__ . ". $Data::Session::errstr"; } # End of create_session_from_q. # ----------------------------------------------- sub create_session_from_scratch { my($self) = @_; return Data::Session -> new ( cache => $self -> cache, data_source => $self -> dsn, data_source_attr => $self -> dsn_attr, directory => $self -> directory, id => $self -> id, id_base => $self -> id_base, id_file => $self -> id_file, id_step => $self -> id_step, password => $self -> password, type => $self -> type, username => $self -> username, verbose => $self -> verbose, ) || die __PACKAGE__ . ". $Data::Session::errstr"; } # End of create_session_from_scratch. # ----------------------------------------------- sub create_table { my($self, $table_name, $id_length) = @_; my($engine) = $self -> engine; my($column_type) = $self -> column_type; my($result) = $self -> creator -> create_table(< 1}); create table $table_name ( id char($id_length) not null primary key, a_session $column_type not null ) $engine SQL } # End of create_table. # ----------------------------------------------- sub dump { my($self) = @_; $self -> log('cache: ' . $self -> cache); $self -> log('column_type: ' . $self -> column_type); $self -> log('creator: ' . $self -> creator); $self -> log('dbh: ' . $self -> dbh); $self -> log('directory: ' . $self -> directory); $self -> log('dsn: ' . $self -> dsn); $self -> log('dsn_attr: ' . $self -> hashref2string($self -> dsn_attr) ); $self -> log('engine: ' . $self -> engine); $self -> log('id: ' . $self -> id); $self -> log('id_base: ' . $self -> id_base); $self -> log('id_file: ' . $self -> id_file); $self -> log('id_step: ' . $self -> id_step); $self -> log('key: ' . $self -> key); $self -> log('password: ' . $self -> password); $self -> log('table_name: ' . $self -> table_name); $self -> log('test_count: ' . $self -> test_count); $self -> log('type: ' . $self -> type); $self -> log('username: ' . $self -> username); $self -> log('value: ' . $self -> value); $self -> log('verbose: ' . $self -> verbose); } # End of dump. # ----------------------------------------------- sub init { my($self, $arg) = @_; $$arg{cache} ||= ''; # new(cache => ...). $$arg{column_type} = ''; $$arg{creator} = ''; $$arg{dbh} = ''; $$arg{directory} ||= File::Spec -> tmpdir; # new(directory => ...). $$arg{dsn} ||= ''; # new(dsn => ...). $$arg{dsn_attr} ||= ''; # new(dsn_attr => ...). $$arg{engine} = ''; $$arg{id} ||= 0; # new(id => ...). $$arg{id_base} ||= 0; # new(id_base => ...). $$arg{id_file} ||= File::Spec -> catdir(File::Spec -> tmpdir, 'data.session.id'); # new(id_file => ...). $$arg{id_step} ||= 1; # new(id_step => ...). $$arg{key} = 'Perl'; $$arg{password} ||= ''; # new(password => ...). $$arg{table_name} = 'sessions'; $$arg{test_count} = 0; # The caller did use_ok on Data::Session. $$arg{type} ||= ''; # new(type => ...). $$arg{username} ||= ''; # new(username => ...). $$arg{value} = 'Language'; $$arg{verbose} ||= 0; # new(verbose => ...). } # End of init. # ----------------------------------------------- sub hashref2string { my($self, $h) = @_; $h ||= {}; return '{' . join(', ', map{"$_ => $$h{$_}"} sort keys %$h) . '}'; } # End of hashref2string. # ----------------------------------------------- sub log { my($self, $s) = @_; $s ||= ''; print STDERR "# $s\n"; } # End of log. # ----------------------------------------------- sub new { my($class, %arg) = @_; $class -> init(\%arg); # Expected format: new(type => 'driver:Pg;id:MD5;serialize:FreezeThaw'). if (! $arg{type}) { die __PACKAGE__ . '. No type specified in $obj -> new(...)'; } # Expected format: new(dsn => 'dbi:Pg:dbname=test'). if (! $arg{dsn}) { die __PACKAGE__ . '. No dsn specified in $obj -> new(...)'; } my($self) = from_hash(bless({}, $class), \%arg); return $self; } # End of new. # ----------------------------------------------- sub run { my($self) = @_; ($self -> verbose > 1) && $self -> dump; # Special code for SQLite. The table /must/ exist. # # However, for tests, we always re-create the table, although # users would not normally do this. The reason is that if a # test is for id:Static, serialize:DataDumper, and the next # test is for serialize::FreezeThaw, the static id means the # 2nd test uses the first id's data, which is in DataDumper format. # # For BerkeleyDB, Files and Memcached, skip, since we do not have database tables. if ($self -> type !~ /driver:(?:BerkeleyDB|File|Memcached)/) { # We rig it to use an id length of 128, since the table # is deleted and re-created below before being written to. $self -> setup_table(128); } my($session1) = $self -> create_session_from_scratch; isa_ok($session1, 'Data::Session', '1st session object'); $self -> test_count($self -> test_count + 1); $self -> log('id 1: ' . $session1 -> id); # For BerkeleyDB, Files and Memcached, skip, since we do not have database tables. if ($self -> type !~ /driver:(?:BerkeleyDB|File|Memcached)/) { # This time use the real length of the ID. $self -> setup_table($session1 -> id_class -> id_length); } # Set up some test data to play with. my($key) = $self -> key; my($value) = $self -> value; $session1 -> param($key => $value); $session1 -> param("$key$key" => "$value$value"); $session1 -> flush; # Create a session using the first session's id. my($session2) = $self -> test_session_from_id($session1); # Create a session using a query object based on the first session. my($session3) = $self -> test_session_from_q($session1); # Test save_param and load_param. my($session4) = $self -> test_save_load_param($session1); # Testing setting a parameter to undef. $self -> test_setting_getting_undef; # Clean up. All sessions must be deleted, otherwise they get flushed by Session::Data's DESTROY. $session1 -> delete; $session2 -> delete; $session3 -> delete; $session4 -> delete; done_testing($self -> test_count); # Return 1 to keep the outer done_testing happy. return 1; } # End of run. # ----------------------------------------------- sub setup_table { my($self, $id_length) = @_; $self -> dbh(DBI -> connect($self -> dsn, $self -> username, $self -> password, $self -> dsn_attr) || die __PACKAGE__ . ". Can't connect to " . $self -> dsn); $self -> creator(DBIx::Admin::CreateTable -> new(dbh => $self -> dbh, verbose => 0) ); my($vendor) = $self -> creator -> db_vendor; $self -> column_type($vendor eq 'ORACLE' ? 'long' : $vendor eq 'POSTGRESQL' ? 'bytea' : 'text'); $self -> engine($vendor =~ /(?:Mysql)/i ? 'engine=innodb' : ''); $self -> creator -> drop_table($self -> table_name); $self -> create_table($self -> table_name, $id_length); if ($self -> table_exists == 0) { die __PACKAGE__ . ". Can't create '" . $self -> table_name . "' table"; } } # End of setup_table. # ----------------------------------------------- sub table_exists { my($self) = @_; my($table_sth) = $self -> dbh -> table_info(undef, undef, '%', 'TABLE'); my($result) = 0; for my $table_data (@{$table_sth -> fetchall_arrayref({})}) { if ($$table_data{'TABLE_NAME'} eq $self -> table_name) { $result = 1; } } return $result; } # End of table_exists. # ----------------------------------------------- sub test_cookie_and_http_header { my($self) = @_; $self -> log; $self -> log("Testing HTTP header generation"); my($session) = $self -> create_session_from_scratch; $session -> expire(10); my($my_header) = $session -> http_header; my($q) = CGI -> new; my($cgi_cookie) = $q -> cookie(-name => 'CGISESSID', -value => $session -> id, -expires => '+10s'); my($cgi_header) = $q -> header(-cookie => $cgi_cookie, -type => 'text/html'); ok($my_header eq $cgi_header, 'HTTP header created via CGI directly matches one via http_header()'); # Return test count. return 1; } # End of test_cookie_and_http_header. # ----------------------------------------------- sub test_expire_a_session_parameter { my($self) = @_; my($count) = 0; my($delay) = 1; # Second. my(%data) = ( key_1 => { expire => 0, value => 'value_1', }, key_2 => { expire => $delay, value => 'value_2', }, ); my($id); # 1: Create a session, and when it goes out of scope, it's saved to storage. { my($session) = $self -> create_session_from_scratch; $id = $session -> id; for my $key (keys %data) { $session -> expire($key => $data{$key}{expire}); $session -> param($key => $data{$key}{value}); } } # 2: Sleep beyond the expiry time, and read the session back in. $self -> log; $self -> log("Testing expire a session parameter. Sleeping for $delay second ..."); $delay = 3 * $delay; sleep($delay); my($session) = $self -> create_session_from_id($id); my($ptime) = $session -> ptime; for my $key (sort keys %$ptime) { $self -> log("Recovered $key: $$ptime{$key}"); } # We should have lost key_2 by now. my($data); for my $key (keys %data) { $data = $session -> param($key); if ($key eq 'key_1') { ok(defined $data, "Data for key $key not expired, and hence retrieved from storage"); } else { ok(! defined $data, "Data for key $key expired, and hence not retrieved from storage"); } # This is not called, because we're running after the inner done_testing(). #$self -> test_count($self -> test_count + 1); $count++; } # Return test count. return $count; } # End of test_expire_a_session_parameter. # ----------------------------------------------- sub test_expire_the_session { my($self) = @_; my($key) = 'Perl'; my($value) = 'Language'; my($count) = 0; my($delay) = 1; # Second. my($id); # 1: Create a session, and when it goes out of scope, it's saved to storage. { my($session) = $self -> create_session_from_scratch; $id = $session -> id; $session -> expire($delay); $session -> param($key => $value); my($secs) = $session -> expire; ok($delay == $secs, 'Expiry time set and retrieved'); # This is not called, because we're running after the inner done_testing(). #$self -> test_count($self -> test_count + 1); $count++; } # 2: Sleep beyond the expiry time, and read the session back in. $self -> log; $self -> log("Testing expire the session. Sleeping for $delay second ..."); $delay = 3 * $delay; sleep($delay); my($session) = $self -> create_session_from_id($id); # We should have lost $key by now. my($data) = $session -> param($key); ok(! defined $data, 'Data expired, and hence not retrieved from storage'); # This is not called, because we're running after the inner done_testing(). #$self -> test_count($self -> test_count + 1); $count++; # Return test count. return $count; } # End of test_expire_the_session. # ----------------------------------------------- sub test_save_load_param { my($self, $session1) = @_; # 1: Stuff some data into a query object. my($q1) = CGI -> new; my(%data) = ( key_1 => 'value_1', key_2 => 'value_2', ); my($key); for $key (keys %data) { $q1 -> param($key => $data{$key}); } # 2: Test save param, copying data from a query object to a session. my($session4) = $self -> create_session_from_scratch; $session4 -> save_param($q1, [keys %data]); my($total1) = ''; my($total2) = ''; for $key (keys %data) { $total1 .= $data{$key}; $total2 .= $session4 -> param($key); } ok($total1 eq $total2, 'Data recovered from save_param() matches'); $self -> test_count($self -> test_count + 1); # 3: Test load param, copying data from a session to a query object. my($q2) = $session4 -> load_param(undef, [keys %data]); $total1 = ''; $total2 = ''; for $key (keys %data) { $total1 .= $data{$key}; $total2 .= $q2 -> param($key); } ok($total1 eq $total2, 'Data recovered from load_param() matches'); $self -> test_count($self -> test_count + 1); return $session4; } # End of test_save_load_param. # ----------------------------------------------- sub test_session_from_id { my($self, $session1) = @_; my($session2) = $self -> create_session_from_id($session1 -> id); isa_ok($session2, 'Data::Session', '2nd session object'); $self -> test_count($self -> test_count + 1); ($self -> verbose > 1) && $self -> log('id 2: ' . $session2 -> id); my($key) = $self -> key; my($data) = $session2 -> param($key); my($value) = $self -> value; ok($value eq $data, "Data stored (session1) and retrieved (session2)"); $self -> test_count($self -> test_count + 1); return $session2; } # End of test_session_from_id. # ----------------------------------------------- sub test_session_from_q { my($self, $session1) = @_; my($session3) = $self -> create_session_from_q($session1); isa_ok($session3, 'Data::Session', '3rd session object'); $self -> test_count($self -> test_count + 1); ($self -> verbose > 1) && $self -> log('id 3: ' . $session3 -> id); my($key) = $self -> key; my($data) = $session3 -> param($key); my($value) = $self -> value; ok($value eq $data, "Data stored (session1) and retrieved (session3)"); $self -> test_count($self -> test_count + 1); $key = "$key$key"; $data = $session3 -> param($key); ok("$value$value" eq $data, "More data stored (session1) and retrieved (session3)"); $self -> test_count($self -> test_count + 1); return $session3; } # End of test_session_from_q. # ----------------------------------------------- sub test_setting_getting_undef { my($self) = @_; my($key1) = 'stealth'; my($value1) = undef; my($key2) = 'null'; my($value2) = 'null'; my($session1) = $self -> create_session_from_scratch; $session1 -> param($key1 => $value1); $session1 -> param($key2 => $value2); $session1 -> flush; my($session2) = $self -> create_session_from_id($session1 -> id); ok(! defined $session2 -> param($key1), 'Stored and retrieved undef'); $self -> test_count($self -> test_count + 1); ok($session2 -> param($key2) eq $value2, "Stored and retrieved 'null'"); $self -> test_count($self -> test_count + 1); $session1 -> delete; $session2 -> delete; } # End of test_setting_getting_undef. # ----------------------------------------------- sub test_validation_of_time_strings { my($self) = @_; my(%map) = ( '-10' => -10, '+10d' => 864000, '10M' => 25920000, ); my($session) = $self -> create_session_from_scratch; my($count) = 0; my($seconds_in, $seconds_out); for my $time (qw/-10 +10d 10M/) { $count++; $seconds_in = $map{$time}; $seconds_out = $session -> validate_time($time); ok($seconds_in == $seconds_out, "Validated time string $time"); # This is not called, because we're running after the inner done_testing(). #$self -> test_count($self -> test_count + 1); } $session -> delete; # Return test count. return $count; } # End of test_validation_of_time_strings. # ----------------------------------------------- sub traverse { my($self) = @_; ($self -> verbose > 1) && $self -> dump; # Special code for SQLite. The table /must/ exist. # # However, for tests, we always re-create the table, although # users would not normally do this. The reason is that if a # test is for id:Static, serialize:DataDumper, and the next # test is for serialize::FreezeThaw, the static id means the # 2nd test uses the first id's data, which is in DataDumper format. # # For Files, skip, since we do not have database tables. if ($self -> type !~ /driver:File/) { # We rig it to use an id length of 32, since the table # is deleted and re-created below before being written to. $self -> setup_table(32); } my($session1) = $self -> create_session_from_scratch; isa_ok($session1, 'Data::Session', '1st session object'); $self -> test_count($self -> test_count + 1); $self -> log('id1: ' . $session1 -> id); # Stash ids for the traversal below. my(%id); $id{$session1 -> id} = 1; # For Files, skip, since we do not have database tables. if ($self -> type !~ /driver:File/) { # This time use the real length of the ID. $self -> setup_table($session1 -> id_class -> id_length); } # Create another 4 sessions, and then run a traverse(). for my $count (1 .. 4) { $session1 = $self -> create_session_from_scratch; $id{$session1 -> id} = 1; # Set some test data to play with. $session1 -> param($self -> key => $self -> value); $session1 -> flush; } my($count) = 0; my($sub) = sub { my($id) = @_; $count++; if ($id{$id}) { $self -> log("$count: Recovered known id $id from traverse"); } else { $self -> log("$count: Recovered unknown id $id from traverse"); } }; $session1 -> traverse($sub); } # End of traverse. # ----------------------------------------------- 1;