#!/usr/bin/perl # Test the RPC::XML::Parser::XMLParser class use strict; use vars qw($p $req $res $ret $dir $vol $file $fh $str $badstr); use Test::More tests => 137; require File::Spec; require IO::File; use RPC::XML ':all'; use RPC::XML::Parser::XMLParser; ($vol, $dir, undef) = File::Spec->splitpath(File::Spec->rel2abs($0)); $dir = File::Spec->catpath($vol, $dir, ''); $file = File::Spec->catfile($dir, 'svsm_text.gif'); # The organization of the test suites is such that we assume anything that # runs before the current suite is 100%. Thus, no consistency checks on # RPC::XML::* classes are done, only on the data and return values of this # class under consideration, RPC::XML::Parser::XMLParser. $p = RPC::XML::Parser::XMLParser->new(); isa_ok($p, 'RPC::XML::Parser::XMLParser', '$p'); isa_ok($p, 'RPC::XML::Parser', '$p'); # Make sure you can't call parse_more() or parse_done() on a vanilla # RPC::XML::Parser::XMLParser instance: $ret = eval { $p->parse_more(); 1; }; ok(! $ret, 'Calling parse_more on $p failed'); like($@, qr/Must be called on a push-parser instance/, 'Correct error message'); $ret = eval { $p->parse_done(); 1; }; ok(! $ret, 'Calling parse_done on $p failed'); like($@, qr/Must be called on a push-parser instance/, 'Correct error message'); $req = RPC::XML::request->new('test.method'); $ret = $p->parse($req->as_string); isa_ok($ret, 'RPC::XML::request', '$ret'); is($ret->name, 'test.method', 'Correct request method name'); # Try a request with no block at all: $str = < test.method EO_STR $ret = $p->parse($str); isa_ok($ret, 'RPC::XML::request', '$ret'); is($ret->name, 'test.method', 'Correct request method name'); ok(ref($ret->args) eq 'ARRAY' && @{$ret->args} == 0, 'No block yields correct args list'); $res = RPC::XML::response->new(RPC::XML::string->new('test response')); $ret = $p->parse($res->as_string); isa_ok($ret, 'RPC::XML::response', '$ret'); is($ret->value->value, 'test response', 'Response value'); # Test some badly-formed data my $tmp = $res->as_string; $tmp =~ s/methodResponse/mR/g; $ret = $p->parse($tmp); ok(! ref($ret), 'Bad XML did not parse'); like($ret, qr/Unknown tag/, 'Parse failure returned error'); # Make sure that the parser can handle all of the core data-types. Easiest way # to do this is to create a fake request with a parameter of each type (except # base64, which is getting exercised later on). $req = RPC::XML::request->new( 'parserTest', RPC::XML::i4->new(1), RPC::XML::int->new(2), RPC::XML::i8->new(3), RPC::XML::double->new(4.5), RPC::XML::string->new('string'), RPC::XML::boolean->new('true'), RPC::XML::datetime_iso8601->new('2008-09-29T12:00:00-07:00'), [ 0, 1 ], # Array, auto-encoded { a => 1, b => 2 }, # Hash/struct, also auto-encoded ); $ret = $p->parse($req->as_string); isa_ok($ret, 'RPC::XML::request', 'Parse of RPC::XML::request block'); SKIP: { skip "RPC::XML::request object not properly parsed, cannot test it.", 20 unless (ref($ret) eq 'RPC::XML::request'); is($ret->name, 'parserTest', 'Properly parsed /methodCall/methodName'); my $args = $ret->args; is(scalar @$args, 9, 'Parser created correct-length args list'); # I could (and should) probably turn this into a loop with a table of # data, but I'm lazy right this moment. isa_ok($args->[0], 'RPC::XML::i4', 'Parse of argument'); is($args->[0]->value, 1, 'RPC::XML::i4 value parsed OK'); isa_ok($args->[1], 'RPC::XML::int', 'Parse of argument'); is($args->[1]->value, 2, 'RPC::XML::int value parsed OK'); isa_ok($args->[2], 'RPC::XML::i8', 'Parse of argument'); is($args->[2]->value, 3, 'RPC::XML::i8 value parsed OK'); isa_ok($args->[3], 'RPC::XML::double', 'Parse of argument'); is($args->[3]->value, 4.5, 'RPC::XML::double value parsed OK'); isa_ok($args->[4], 'RPC::XML::string', 'Parse of argument'); is($args->[4]->value, 'string', 'RPC::XML::string value parsed OK'); isa_ok($args->[5], 'RPC::XML::boolean', 'Parse of argument'); ok($args->[5]->value, 'RPC::XML::boolean value parsed OK'); isa_ok($args->[6], 'RPC::XML::datetime_iso8601', 'Parse of argument'); is($args->[6]->value, '20080929T12:00:00-07:00', 'RPC::XML::dateTime.iso8601 value parsed OK'); isa_ok($args->[7], 'RPC::XML::array', 'Parse of argument'); is(scalar(@{$args->[7]->value}), 2, 'RPC::XML::array value parsed OK'); isa_ok($args->[8], 'RPC::XML::struct', 'Parse of argument'); is(scalar(keys %{$args->[8]->value}), 2, 'RPC::XML::struct value parsed OK'); } # Prior to this, we've confirmed that spooling base64 data to files works. # Here, we test whether the parser (when configured to do so) can create # filehandles as well. undef $p; $p = RPC::XML::Parser::XMLParser->new(base64_to_fh => 1); $fh = IO::File->new("< $file"); die "Error opening $file: $!" unless ref $fh; my $base64 = RPC::XML::base64->new($fh); $req = RPC::XML::request->new('method', $base64); # Start testing my $spool_ret = $p->parse($req->as_string); isa_ok($spool_ret, 'RPC::XML::request', '$spool_ret'); is($spool_ret->name, 'method', 'Request, base64 spooling, method name test'); ok(ref($spool_ret->args), 'Request, base64 spooling, return arg test'); my $new_base64 = $spool_ret->args->[0]; isa_ok($new_base64, 'RPC::XML::base64', '$new_base64'); is($base64->as_string(), $new_base64->as_string, 'Parse base64 spooling, value comparison'); isa_ok($new_base64->{value_fh}, 'GLOB', '$new_base64->{value_fh}'); # Per problem reported by Bill Moseley, check that messages parsed by the # parser class handle the core entities. $tmp = q{Entity test: & < > ' "}; $res = RPC::XML::response->new($tmp); $ret = $p->parse($res->as_string); is($ret->value->value, $tmp, 'RPC::XML::Parser handles core entities'); my $bad_entities = < ]> metaWeblog.newPost Entity test: &foo; EOX $p = RPC::XML::Parser::XMLParser->new(); $ret = $p->parse($bad_entities); SKIP: { skip 'Weird entities parsing error in XML::Parser encountered', 1 if (! ref $ret); my $args = $ret->args; is($args->[0]->value, 'Entity test: ', 'Bad entities ignored'); } # Now test passing of various references to the parser $p = RPC::XML::Parser::XMLParser->new(); $str = RPC::XML::request->new('test.method')->as_string; $ret = $p->parse(\$str); isa_ok($ret, 'RPC::XML::request', '$ret from scalar reference'); ok(ref($ret) && ($ret->name eq 'test.method'), 'Correct request method name'); my $tmpfile = File::Spec->catfile($dir, "tmp_$$.xml"); open $fh, '+>', $tmpfile; SKIP: { skip "Open of $tmpfile failed, cannot test on it ($!)", 2 if (! $fh); print {$fh} $str; seek $fh, 0, 0; $ret = $p->parse($fh); isa_ok($ret, 'RPC::XML::request', '$ret from glob reference'); ok((ref($ret) and ($ret->name eq 'test.method')), 'Correct request method name'); close $fh; unlink $tmpfile; } # Tweak the XML to test the error cases $str =~ s{}{}; $ret = $p->parse(\$str); ok(! ref $ret, '$ret error from scalar reference'); like($ret, qr/no element found/, 'Correct error message'); open $fh, '+>', $tmpfile; SKIP: { skip "Open of $tmpfile failed, cannot test on it ($!)", 2 if (! $fh); print {$fh} $str; seek $fh, 0, 0; $ret = $p->parse($fh); ok(! ref $ret, '$ret error from glob reference'); like($ret, qr/no element found/, 'Correct error message'); close $fh; unlink $tmpfile; } # Try an unusable reference $ret = $p->parse([]); ok(! ref $ret, 'Unusable reference did not parse to anything'); like($ret, qr/Unusable reference type/, 'Correct error message'); # Negative testing-- try to break the parser my $bad_counter = 1; sub test_bad_xml { my ($badstr, $message) = @_; $ret = $p->parse($badstr); ok(! ref $ret, "Bad XML <$bad_counter>"); like($ret, qr/$message/, 'Correct error message'); $bad_counter++; } $str = RPC::XML::request->new('name', 'foo')->as_string; ($badstr = $str) =~ s/>namebad^name.*}{}; test_bad_xml($badstr, 'No methodName tag detected'); ($badstr = $str) =~ s{}{}; test_bad_xml($badstr, 'Extra content in "methodCall"'); ($badstr = $str) =~ s{params>}{paramss>}g; test_bad_xml($badstr, 'Unknown tag encountered: paramss'); $str = RPC::XML::response->new(1)->as_string; ($badstr = $str) =~ s{}{}; test_bad_xml($badstr, 'Stack corruption detected'); ($badstr = $str) =~ s{}{}; test_bad_xml($badstr, 'No found within container'); ($badstr = $str) =~ s{param>}{paramm>}g; test_bad_xml($badstr, 'Unknown tag encountered: paramm'); ($badstr = $str) =~ s{}{}; test_bad_xml($badstr, 'Illegal content in param tag'); ($badstr = $str) =~ s{value>}{valuee>}g; test_bad_xml($badstr, 'Unknown tag encountered: valuee'); ($badstr = $str) =~ s{>1<}{>foo<}; test_bad_xml($badstr, 'Bad integer'); ($badstr = $str) =~ s{params}{paramss}g; test_bad_xml($badstr, 'Unknown tag encountered: paramss'); $str = RPC::XML::response->new(RPC::XML::fault->new(1, 'foo'))->as_string; ($badstr = $str) =~ s{}{}; test_bad_xml($badstr, 'Stack corruption detected'); ($badstr = $str) =~ s{}{}; $badstr =~ s{}{}; test_bad_xml($badstr, 'Unknown tag encountered: valuee'); # These are a little more hairy, trying to pass an invalid fault structure. # Gonna hard-code the strings rather than trying to transform $str. $badstr = < str faultString foo faultCode 1 EO_BADSTR test_bad_xml($badstr, 'Bad content inside struct block'); $badstr = < faultString foo faultCode 1 extraMember 1 EO_BADSTR test_bad_xml($badstr, 'Extra struct fields not allowed'); $badstr = < EO_BADSTR test_bad_xml($badstr, 'Stack corruption detected'); $badstr = < foo EO_BADSTR test_bad_xml($badstr, 'Only a value may be within a '); $RPC::XML::ALLOW_NIL = 1; $str = RPC::XML::response->new(undef)->as_string; ($badstr = $str) =~ s{}{undef}; test_bad_xml($badstr, ' element must be empty'); $str = RPC::XML::request->new('foo', 1)->as_string; ($badstr = $str) =~ s{}{}; test_bad_xml($badstr, 'Illegal content in params tag'); ($badstr = $str) =~ s{.*}{}; test_bad_xml($badstr, 'Illegal content in params tag'); ($badstr = $str) =~ s{}{}; $badstr =~ s{}{}; test_bad_xml($badstr, 'Unknown tag encountered: valuee'); ($badstr = $str) =~ s{}{1}; test_bad_xml($badstr, 'Stack corruption detected'); ($badstr = $str) =~ s{1}{foo}; test_bad_xml($badstr, 'Bad floating-point data read'); # Parser errors specific to arrays: $str = RPC::XML::response->new([ 1 ])->as_string; ($badstr = $str) =~ s{}{}; test_bad_xml($badstr, 'Illegal content in array tag'); ($badstr = $str) =~ s{}{}; $badstr =~ s{}{}; test_bad_xml($badstr, 'Unknown tag encountered: valuee'); ($badstr = $str) =~ s{1}{foo}; test_bad_xml($badstr, 'Bad integer data read'); $badstr = < 1 foo EO_BADSTR test_bad_xml($badstr, 'Bad content inside data block'); $badstr = < foo 1 EO_BADSTR test_bad_xml($badstr, 'Illegal content in data tag'); # Parser errors specific to structs: $str = RPC::XML::response->new({ foo => 1 })->as_string; ($badstr = $str) =~ s{}{}; test_bad_xml($badstr, 'Unknown tag encountered: foo'); ($badstr = $str) =~ s{name>}{namee>}g; test_bad_xml($badstr, 'Unknown tag encountered: namee'); ($badstr = $str) =~ s{1}{foo}; test_bad_xml($badstr, 'Bad integer data'); $badstr = < foo 1 1 EO_BADSTR test_bad_xml($badstr, 'Element mismatch, expected to see name'); $badstr = < 1 foo EO_BADSTR test_bad_xml($badstr, 'Element mismatch, expected to see value'); $badstr = < foo 1 1 EO_BADSTR test_bad_xml($badstr, 'Element mismatch, expected to see member'); $badstr = < 1 foo 1 EO_BADSTR test_bad_xml($badstr, 'Bad content inside struct block'); # Some corner-cases in responses $badstr = < 1 1 EO_BADSTR test_bad_xml($badstr, 'invalid: too many params'); $badstr = < EO_BADSTR test_bad_xml($badstr, 'invalid: no params'); $badstr = < EO_BADSTR test_bad_xml($badstr, 'No parameter was declared'); # Corner case(s) in requests $badstr = < foo foo EO_BADSTR test_bad_xml($badstr, 'methodName tag must immediately follow a methodCall'); # Test the "none of the above" error case ($badstr = $str) =~ s/struct/structt/g; test_bad_xml($badstr, 'Unknown tag encountered: structt'); # Test parse-end errors $badstr = < 1 EO_BADSTR test_bad_xml($badstr, 'End-of-parse error'); # Test some of the failures related to Base64-spooling. This can only be tested # on non-Windows systems, as to cause some of the failures we'll need to create # an un-writable directory (and Windows doesn't have the same chmod concept we # have in other places). SKIP: { skip 'Tests involving directory permissions skipped on Windows', 1 if ($^O eq 'MSWin32' || $^O eq 'cygwin'); skip 'Tests involving directory permissions skipped under root', 1 if ($< == 0); my $baddir = File::Spec->catdir(File::Spec->tmpdir(), "baddir_$$"); if (! mkdir $baddir) { skip "Skipping, failed to create dir $baddir: $!", 1; } if (! chmod oct(600), $baddir) { skip "Skipping, failed to chmod dir $baddir: $!", 1; } undef $p; $p = RPC::XML::Parser::XMLParser->new( base64_to_fh => 1, base64_temp_dir => $baddir ); open $fh, '<', $file; die "Error opening $file: $!" unless $fh; my $base64 = RPC::XML::base64->new($fh); $req = RPC::XML::request->new('method', $base64); $ret = $p->parse($req->as_string); like($ret, qr/Error opening temp file for base64/, 'Opening Base64 spoolfile correctly failed'); if (! rmdir $baddir) { warn "Failed to remove temp-dir $baddir: $!"; } } exit 0;