# This code was forked from the LiveJournal project owned and operated # by Live Journal, Inc. The code has been modified and expanded by # Dreamwidth Studios, LLC. These files were originally licensed under # the terms of the license supplied by Live Journal, Inc, which can # currently be found at: # # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt # # In accordance with the original license, this code and all its # modifications are provided under the GNU General Public License. # A copy of that license can be found in the LICENSE file included as # part of this distribution. package LJ::Test; require Exporter; use strict; use Carp qw(croak); use DW::Routing; use DW::Request::Standard; use HTTP::Request; use DBI; use LJ::Utils qw(rand_chars); use LJ::ModuleCheck; our @ISA = qw(Exporter); our @EXPORT = qw(memcache_stress with_fake_memcache temp_user temp_comm temp_feed routing_request); my @temp_userids; # to be destroyed later END { return if $LJ::_T_NO_TEMP_USER_DESTROY; # clean up temporary usernames foreach my $uid (@temp_userids) { my $u = LJ::load_userid($uid) or next; $u->delete_and_purge_completely; } } $LJ::_T_FAKESCHWARTZ = 1 unless $LJ::_T_NOFAKESCHWARTZ; my $theschwartz = undef; sub theschwartz { return $theschwartz if $theschwartz; my $fakedb = "$LJ::HOME/t-theschwartz.sqlite"; unlink $fakedb, "$fakedb-journal"; my $fakedsn = "dbi:SQLite:dbname=$fakedb"; my $load_sql = sub { my ($file) = @_; open my $fh, $file or die "Can't open $file: $!"; my $sql = do { local $/; <$fh> }; close $fh; split /;\s*/, $sql; }; my $dbh = DBI->connect( $fakedsn, '', '', { RaiseError => 1, PrintError => 0 } ); my @sql = $load_sql->("$LJ::HOME/t/data/schema-sqlite.sql"); for my $sql (@sql) { $dbh->do($sql); } $dbh->disconnect; return $theschwartz = TheSchwartz->new( databases => [ { dsn => $fakedsn, user => '', pass => '', } ] ); } sub temp_user { shift() if defined( $_[0] ) and $_[0] eq __PACKAGE__; my %args = @_; my $underscore = delete $args{'underscore'}; my $journaltype = delete $args{'journaltype'} || "P"; my $cluster = delete $args{cluster}; croak('unknown args') if %args; my $pfx = $underscore ? "_" : "t_"; while (1) { my $username = $pfx . LJ::rand_chars( 15 - length $pfx ); my $u = LJ::User->create( user => $username, name => "test account $username", email => "test\@$LJ::DOMAIN", journaltype => $journaltype, cluster => $cluster, ); next unless $u; push @temp_userids, $u->id; return $u; } } sub temp_comm { shift() if defined( $_[0] ) and $_[0] eq __PACKAGE__; # make a normal user my $u = temp_user(); # update journaltype $u->update_self( { journaltype => 'C' } ); # communities always have a row in 'community' my $dbh = LJ::get_db_writer(); $dbh->do( "INSERT INTO community SET userid=?", undef, $u->{userid} ); die $dbh->errstr if $dbh->err; return $u; } sub temp_feed { shift() if defined( $_[0] ) and $_[0] eq __PACKAGE__; # make a normal user my $u = temp_user(); # update journaltype $u->update_self( { journaltype => 'Y' } ); # communities always have a row in 'syndicated' my $dbh = LJ::get_db_writer(); $dbh->do( "INSERT INTO syndicated (userid, synurl, checknext) VALUES (?,?,NOW())", undef, $u->id, "$LJ::SITEROOT/fakerss.xml#" . $u->user ); die $dbh->errstr if $dbh->err; return $u; } sub with_fake_memcache (&) { my $cb = shift; my $pre_mem = LJ::MemCache::get_memcache(); my $fake_memc = LJ::Test::FakeMemCache->new(); { local @LJ::MEMCACHE_SERVERS = ("fake"); LJ::MemCache::set_memcache($fake_memc); $cb->(); } # restore our memcache client object from before. LJ::MemCache::set_memcache($pre_mem); } sub memcache_stress (&) { my $cb = shift; my $pre_mem = LJ::MemCache::get_memcache(); my $fake_memc = LJ::Test::FakeMemCache->new(); # run the callback once with no memcache server existing { local @LJ::MEMCACHE_SERVERS = (); LJ::MemCache::init(); $cb->(); } # now set a memcache server, but a new empty one, and run it twice # so the second invocation presumably has stuff in the cache # from the first one { local @LJ::MEMCACHE_SERVERS = ("fake"); LJ::MemCache::set_memcache($fake_memc); $cb->(); $cb->(); } # restore our memcache client object from before. LJ::MemCache::set_memcache($pre_mem); } # this is a quick check for whether memcache is functioning correctly # using a bogus wsse_auth key - add fails when not working as configured sub check_memcache { return 1 unless @LJ::MEMCACHE_SERVERS; # OK if not set my $secs = time; return LJ::MemCache::add( "wsse_auth:xxx:$secs", 1, 1 ); } sub routing_request { my ( $uri, %opts ) = @_; my $method = $opts{method} || 'GET'; my %routing_data = %{ $opts{routing_data} || {} }; LJ::start_request(); my $req = HTTP::Request->new( $method => $uri ); if ( $opts{content} ) { $req->content( $opts{content} ); $req->header( 'Content-Length', length( $opts{content} ) ); } $opts{setup_http_request}->($req) if $opts{setup_http_request}; # Just in case, but this shouldn't get set in a non-web context DW::Request->reset; my $r = DW::Request::Standard->new($req); $opts{setup_dw_request}->($r) if $opts{setup_dw_request}; my $rv = DW::Routing->call(%routing_data); $r->status($rv) unless $rv eq $r->OK; return $r; } package LJ::Test::FakeMemCache; # duck-typing at its finest! # this is a fake Cache::Memcached object which implements the # memcached server locally in-process, for testing. kinda, # except it has no LRU or expiration times. sub new { my ($class) = @_; return bless { 'data' => {}, }, $class; } sub add { my ( $self, $fkey, $val, $exptime ) = @_; my $key = _key($fkey); return 0 if exists $self->{data}{$key}; $self->{data}{$key} = $val; return 1; } sub replace { my ( $self, $fkey, $val, $exptime ) = @_; my $key = _key($fkey); return 0 unless exists $self->{data}{$key}; $self->{data}{$key} = $val; return 1; } sub incr { my ( $self, $fkey, $optval ) = @_; $optval ||= 1; my $key = _key($fkey); return 0 unless exists $self->{data}{$key}; $self->{data}{$key} += $optval; return $self->{data}{$key}; } sub decr { my ( $self, $fkey, $optval ) = @_; $optval ||= 1; my $key = _key($fkey); return 0 unless exists $self->{data}{$key}; $self->{data}{$key} -= $optval; return 1; } sub set { my ( $self, $fkey, $val, $exptime ) = @_; my $key = _key($fkey); $self->{data}{$key} = $val; return 1; } sub delete { my ( $self, $fkey ) = @_; my $key = _key($fkey); delete $self->{data}{$key}; return 1; } sub get { my ( $self, $fkey ) = @_; my $key = _key($fkey); return $self->{data}{$key}; } sub get_multi { my $self = shift; my $ret = {}; foreach my $fkey (@_) { my $key = _key($fkey); $ret->{$key} = $self->{data}{$key} if exists $self->{data}{$key}; } return $ret; } sub _key { my $fkey = shift; return $fkey->[1] if ref $fkey eq "ARRAY"; return $fkey; } # tell LJ::MemCache::reload_conf not to call 'weird' methods on us # that we don't simulate. sub doesnt_want_configuration { 1; } sub disconnect_all { } sub forget_dead_hosts { } package LJ::User; # post a fake entry in a community journal sub t_post_fake_comm_entry { my $u = shift; my $comm = shift; my %opts = @_; # set the 'usejournal' and tell the protocol # to not do any checks for posting access $opts{usejournal} = $comm->{user}; $opts{usejournal_okay} = 1; return $u->t_post_fake_entry(%opts); } # post a fake entry in this user's journal sub t_post_fake_entry { my $u = shift; my %opts = @_; use LJ::Protocol; my $security = delete $opts{security} || 'public'; my $proto_sec = $security; if ( $security eq "friends" ) { $proto_sec = "usemask"; } my $subject = delete $opts{subject} || "test suite post."; my $body = delete $opts{body} || "This is a test post from $$ at " . time() . "\n"; my %req = ( mode => 'postevent', ver => $LJ::PROTOCOL_VER, user => $u->{user}, password => '', event => $body, subject => $subject, tz => 'guess', security => $proto_sec, ); $req{allowmask} = 1 if $security eq 'friends'; my %res; my $flags = { noauth => 1, nomod => 1 }; # pass-thru opts $req{usejournal} = $opts{usejournal} if $opts{usejournal}; $flags->{usejournal_okay} = $opts{usejournal_okay} if $opts{usejournal_okay}; LJ::do_request( \%req, \%res, $flags ); die "Error posting: $res{errmsg}" unless $res{'success'} eq "OK"; my $jitemid = $res{itemid} or die "No itemid"; my $ju = $opts{usejournal} ? LJ::load_user( $opts{usejournal} ) : $u; return LJ::Entry->new( $ju, jitemid => $jitemid ); } package LJ::Entry; use LJ::Talk; # returns LJ::Comment object or dies on failure sub t_enter_comment { my ( $entry, %opts ) = @_; my $jitemid = $entry->jitemid; # entry journal/u my $entryu = $entry->journal; # poster u my $u = delete $opts{u}; $u = 0 unless ref $u; my $parent = delete $opts{parent}; my $parenttalkid = $parent ? $parent->jtalkid : 0; # add some random stuff for dupe protection my $rand = "t=" . time() . " r=" . rand(); my $subject = delete $opts{subject} || "comment subject [$rand]"; my $body = delete $opts{body} || "comment body\n\n$rand"; my $err; my $commentref = { u => $u, parent => { talkid => $parenttalkid, state => 'A' }, entry => $entry, state => 'A', subject => $subject, body => $body, %opts, parenttalkid => $parenttalkid, }; my ( $ok, $talkid_or_err ) = LJ::Talk::Post::post_comment($commentref); unless ($ok) { die "Could not post comment: $talkid_or_err"; } delete $entry->{_loaded_comments}; delete $entry->{_loaded_talkdata}; return LJ::Comment->new( $entryu, jtalkid => $talkid_or_err ); } package LJ::Comment; # reply to a comment instance, takes same opts as LJ::Entry::t_enter_comment sub t_reply { my ( $comment, %opts ) = @_; my $entry = $comment->entry; $opts{parent} = $comment; return $entry->t_enter_comment(%opts); } 1;