3 if (php_sapi_name() != 'cli') {
7 define('HTTP_TIMEOUT', 60); // ssslllloowwwww salmon if queues are off
9 define('INSTALLDIR', dirname(dirname(dirname(dirname(__FILE__)))));
10 set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path());
12 require_once 'PEAR.php';
13 require_once 'Net/URL2.php';
14 require_once 'HTTP/Request2.php';
17 // ostatus test script, client-side :)
23 $args = func_get_args();
26 $msg = vsprintf($str, $args);
30 function assertEqual($a, $b)
33 throw new Exception("Failed to assert equality: expected $a, got $b");
38 function assertNotEqual($a, $b)
41 throw new Exception("Failed to assert inequality: expected not $a, got $b");
46 function assertTrue($a)
49 throw new Exception("Failed to assert true: got false");
53 function assertFalse($a)
56 throw new Exception("Failed to assert false: got true");
61 class OStatusTester extends TestBase
64 * @param string $a base URL of test site A (eg http://localhost/mublog)
65 * @param string $b base URL of test site B (eg http://localhost/mublog2)
66 * @param int $timeout HTTP timeout value (needs to be long if Salmon is slow)
68 function __construct($a, $b, $timeout=60) {
72 $base = 'test' . mt_rand(1, 1000000);
73 $this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);
74 $this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);
76 $this->group = 'group' . $base;
83 $methods = get_class_methods($this);
84 foreach ($methods as $method) {
85 if (strtolower(substr($method, 0, 4)) == 'test') {
87 print "== $method ==\n";
88 call_user_func(array($this, $method));
98 $this->pub->register();
99 $this->pub->assertRegistered();
101 $this->sub->register();
102 $this->sub->assertRegistered();
105 function testLocalPost()
107 $post = $this->pub->post("Local post, no subscribers yet.");
108 $this->assertNotEqual('', $post);
110 $post = $this->sub->post("Local post, no subscriptions yet.");
111 $this->assertNotEqual('', $post);
117 function testMentionUrl()
119 $bits = parse_url($this->b);
120 $base = $bits['host'];
121 if (isset($bits['path'])) {
122 $base .= $bits['path'];
124 $name = $this->sub->username;
126 $post = $this->pub->post("@$base/$name should have this in home and replies");
127 $this->sub->assertReceived($post);
130 function testSubscribe()
132 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
133 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
134 $this->sub->subscribe($this->pub->getProfileLink());
135 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
136 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
141 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
142 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
144 $name = $this->sub->username;
145 $post = $this->pub->post("Regular post, which $name should get via PuSH");
146 $this->sub->assertReceived($post);
149 function testMentionSubscribee()
151 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
152 $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri()));
154 $name = $this->pub->username;
155 $post = $this->sub->post("Just a quick note back to my remote subscribee @$name");
156 $this->pub->assertReceived($post);
159 function testUnsubscribe()
161 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
162 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
163 $this->sub->unsubscribe($this->pub->getProfileLink());
164 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
165 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
168 function testCreateGroup()
170 $this->groupUrl = $this->pub->createGroup($this->group);
171 $this->assertTrue(!empty($this->groupUrl));
174 function testJoinGroup()
176 #$this->assertFalse($this->sub->inGroup($this->groupUrl));
177 $this->sub->joinGroup($this->groupUrl);
178 #$this->assertTrue($this->sub->inGroup($this->groupUrl));
181 function testLocalGroupPost()
183 $post = $this->pub->post("Group post from local to !{$this->group}, should go out over push.");
184 $this->assertNotEqual('', $post);
185 $this->sub->assertReceived($post);
188 function testRemoteGroupPost()
190 $post = $this->sub->post("Group post from remote to !{$this->group}, should come in over salmon.");
191 $this->assertNotEqual('', $post);
192 $this->pub->assertReceived($post);
195 function testLeaveGroup()
197 #$this->assertTrue($this->sub->inGroup($this->groupUrl));
198 $this->sub->leaveGroup($this->groupUrl);
199 #$this->assertFalse($this->sub->inGroup($this->groupUrl));
203 class SNTestClient extends TestBase
205 function __construct($base, $username, $password, $timeout=60)
207 $this->basepath = $base;
208 $this->username = $username;
209 $this->password = $password;
210 $this->timeout = $timeout;
212 $this->fullname = ucfirst($username) . ' Smith';
213 $this->homepage = 'http://example.org/' . $username;
214 $this->bio = 'Stub account for OStatus tests.';
215 $this->location = 'Montreal, QC';
219 * Make a low-level web hit to this site, with authentication.
220 * @param string $path URL fragment for something under the base path
221 * @param array $params POST parameters to send
222 * @param boolean $auth whether to include auth data
224 * @throws Exception on low-level error conditions
226 protected function hit($path, $params=array(), $auth=false, $cookies=array())
228 $url = $this->basepath . '/' . $path;
230 $http = new HTTP_Request2($url, 'POST', array('timeout' => $this->timeout));
232 $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC);
234 foreach ($cookies as $name => $val) {
235 $http->addCookie($name, $val);
237 $http->addPostParameter($params);
238 $response = $http->send();
240 $code = $response->getStatus();
241 if ($code < '200' || $code >= '400') {
242 throw new Exception("Failed API hit to $url: $code\n" . $response->getBody());
249 * Make a hit to a web form, without authentication but with a session.
250 * @param string $path URL fragment relative to site base
251 * @param string $form id of web form to pull initial parameters from
252 * @param array $params POST parameters, will be merged with defaults in form
254 protected function web($path, $form, $params=array())
256 $url = $this->basepath . '/' . $path;
257 $http = new HTTP_Request2($url, 'GET', array('timeout' => $this->timeout));
258 $response = $http->send();
260 $dom = $this->checkWeb($url, 'GET', $response);
262 foreach ($response->getCookies() as $cookie) {
263 // @fixme check for expirations etc
264 $cookies[$cookie['name']] = $cookie['value'];
267 $form = $dom->getElementById($form);
269 throw new Exception("Form $form not found on $url");
271 $inputs = $form->getElementsByTagName('input');
272 foreach ($inputs as $item) {
273 $type = $item->getAttribute('type');
274 if ($type != 'check') {
275 $name = $item->getAttribute('name');
276 $val = $item->getAttribute('value');
277 if ($name && $val && !isset($params[$name])) {
278 $params[$name] = $val;
283 $response = $this->hit($path, $params, false, $cookies);
284 $dom = $this->checkWeb($url, 'POST', $response);
289 protected function checkWeb($url, $method, $response)
291 $dom = new DOMDocument();
292 if (!$dom->loadHTML($response->getBody())) {
293 throw new Exception("Invalid HTML from $method to $url");
296 $xpath = new DOMXPath($dom);
297 $error = $xpath->query('//p[@class="error"]');
298 if ($error && $error->length) {
299 throw new Exception("Error on $method to $url: " .
300 $error->item(0)->textContent);
306 protected function parseXml($path, $body)
308 $dom = new DOMDocument();
309 if ($dom->loadXML($body)) {
312 throw new Exception("Bogus XML data from $path:\n$body");
317 * Make a hit to a REST-y XML page on the site, without authentication.
318 * @param string $path URL fragment for something relative to base
319 * @param array $params POST parameters to send
320 * @return DOMDocument
321 * @throws Exception on low-level error conditions
323 protected function xml($path, $params=array())
325 $response = $this->hit($path, $params, true);
326 $body = $response->getBody();
327 return $this->parseXml($path, $body);
330 protected function parseJson($path, $body)
332 $data = json_decode($body, true);
333 if ($data !== null) {
334 if (!empty($data['error'])) {
335 throw new Exception("JSON API returned error: " . $data['error']);
339 throw new Exception("Bogus JSON data from $path:\n$body");
344 * Make an API hit to this site, with authentication.
345 * @param string $path URL fragment for something under 'api' folder
346 * @param string $style one of 'json', 'xml', or 'atom'
347 * @param array $params POST parameters to send
348 * @return mixed associative array for JSON, DOMDocument for XML/Atom
349 * @throws Exception on low-level error conditions
351 protected function api($path, $style, $params=array())
353 $response = $this->hit("api/$path.$style", $params, true);
354 $body = $response->getBody();
355 if ($style == 'json') {
356 return $this->parseJson($path, $body);
357 } else if ($style == 'xml' || $style == 'atom') {
358 return $this->parseXml($path, $body);
360 throw new Exception("API needs to be JSON, XML, or Atom");
365 * Register the account.
367 * Unfortunately there's not an API method for registering, so we fake it.
371 $this->log("Registering user %s on %s",
374 $ret = $this->web('main/register', 'form_register',
375 array('nickname' => $this->username,
376 'password' => $this->password,
377 'confirm' => $this->password,
378 'fullname' => $this->fullname,
379 'homepage' => $this->homepage,
382 'submit' => 'Register'));
386 * @return string canonical URI/URL to profile page
388 function getProfileUri()
390 $data = $this->api('account/verify_credentials', 'json');
392 return $this->basepath . '/user/' . $id;
396 * @return string human-friendly URL to profile page
398 function getProfileLink()
400 return $this->basepath . '/' . $this->username;
404 * Check that the account has been registered and can be used.
405 * On failure, throws a test failure exception.
407 function assertRegistered()
409 $this->log("Confirming %s is registered on %s",
412 $data = $this->api('account/verify_credentials', 'json');
413 $this->assertEqual($this->username, $data['screen_name']);
414 $this->assertEqual($this->fullname, $data['name']);
415 $this->assertEqual($this->homepage, $data['url']);
416 $this->assertEqual($this->bio, $data['description']);
417 $this->log(" looks good!");
421 * Post a given message from this account
422 * @param string $message
423 * @return string URL/URI of notice
424 * @todo reply, location options
426 function post($message)
428 $this->log("Posting notice as %s on %s: %s",
432 $data = $this->api('statuses/update', 'json',
433 array('status' => $message));
435 $url = $this->basepath . '/notice/' . $data['id'];
440 * Check that this account has received the notice.
441 * @param string $notice_uri URI for the notice to check for
443 function assertReceived($notice_uri)
448 $ok = $this->checkReceived($notice_uri);
454 $this->log(" didn't see it yet, waiting $timeout seconds");
458 throw new Exception(" message $notice_uri not received by $this->username");
462 * Pull the user's home timeline to check if a notice with the given
463 * source URL has been received recently.
464 * If we don't see it, we'll try a couple more times up to 10 seconds.
466 * @param string $notice_uri
468 function checkReceived($notice_uri)
470 $this->log("Checking if %s on %s received notice %s",
475 $dom = $this->api('statuses/home_timeline', 'atom', $params);
477 $xml = simplexml_import_dom($dom);
481 if (is_array($xml->entry)) {
482 $entries = $xml->entry;
484 $entries = array($xml->entry);
486 foreach ($entries as $entry) {
487 if ($entry->id == $notice_uri) {
488 $this->log(" found it $notice_uri");
496 * @param string $profile user page link or webfinger
498 function subscribe($profile)
500 // This uses the command interface, since there's not currently
501 // a friendly Twit-API way to do a fresh remote subscription and
502 // the web form's a pain to use.
503 $this->post('follow ' . $profile);
507 * @param string $profile user page link or webfinger
509 function unsubscribe($profile)
511 // This uses the command interface, since there's not currently
512 // a friendly Twit-API way to do a fresh remote subscription and
513 // the web form's a pain to use.
514 $this->post('leave ' . $profile);
518 * Check that this account is subscribed to the given profile.
519 * @param string $profile_uri URI for the profile to check for
522 function hasSubscription($profile_uri)
524 $this->log("Checking if $this->username has a subscription to $profile_uri");
526 $me = $this->getProfileUri();
527 return $this->checkSubscription($me, $profile_uri);
531 * Check that this account is subscribed to by the given profile.
532 * @param string $profile_uri URI for the profile to check for
535 function hasSubscriber($profile_uri)
537 $this->log("Checking if $this->username is subscribed to by $profile_uri");
539 $me = $this->getProfileUri();
540 return $this->checkSubscription($profile_uri, $me);
543 protected function checkSubscription($subscriber, $subscribed)
545 // Using FOAF as the API methods for checking the social graph
546 // currently are unfriendly to remote profiles
547 $ns_foaf = 'http://xmlns.com/foaf/0.1/';
548 $ns_sioc = 'http://rdfs.org/sioc/ns#';
549 $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
551 $dom = $this->xml($this->username . '/foaf');
552 $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent');
553 foreach ($agents as $agent) {
554 $agent_uri = $agent->getAttributeNS($ns_rdf, 'about');
555 if ($agent_uri == $subscriber) {
556 $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows');
557 foreach ($follows as $follow) {
558 $target = $follow->getAttributeNS($ns_rdf, 'resource');
559 if ($target == ($subscribed . '#acct')) {
560 $this->log(" confirmed $subscriber subscribed to $subscribed");
564 $this->log(" we found $subscriber but they don't follow $subscribed");
568 $this->log(" can't find $subscriber in {$this->username}'s social graph.");
573 * Create a group on this site.
575 * @param string $nickname
576 * @param array $options
577 * @return string: profile URL for the group
579 function createGroup($nickname, $options=array()) {
580 $this->log("Creating group as %s on %s: %s",
585 $data = $this->api('statusnet/groups/create', 'json',
586 array_merge(array('nickname' => $nickname), $options));
590 $this->log(' created as %s', $url);
592 $this->log(' failed? %s', var_export($data, true));
597 function groupInfo($nickname) {
598 $data = $this->api('statusnet/groups/show', 'json', array(
606 * @param string $group nickname or URL
608 function joinGroup($group) {
609 $this->post('join ' . $group);
615 * @param string $group nickname or URL
617 function leaveGroup($group) {
618 $this->post('drop ' . $group);
623 * @param string $nickname
626 function inGroup($nickname) {
631 // @fixme switch to commandline.inc?
632 $timeout = HTTP_TIMEOUT;
636 foreach (array_slice($_SERVER['argv'], 1) as $arg) {
637 if (substr($arg, 0, 2) == '--') {
638 $bits = explode('=', substr($arg, 2), 2);
639 if (count($bits == 2)) {
640 list($key, $val) = $bits;
641 $options[$key] = $val;
644 $options[$key] = true;
650 if (count($args) < 2) {
652 remote-tests.php [options] <url1> <url2>
653 --timeout=## change HTTP timeout from default {$timeout}s
654 url1: base URL of a StatusNet instance
655 url2: base URL of another StatusNet instance
657 This will register user accounts on the two given StatusNet instances
658 and run some tests to confirm that OStatus subscription and posting
659 between the two sites works correctly.
667 if (isset($options['timeout'])) {
668 $timeout = intval($options['timeout']);
671 $tester = new OStatusTester($a, $b, $timeout);