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);
81 $methods = get_class_methods($this);
82 foreach ($methods as $method) {
83 if (strtolower(substr($method, 0, 4)) == 'test') {
85 print "== $method ==\n";
86 call_user_func(array($this, $method));
96 $this->pub->register();
97 $this->pub->assertRegistered();
99 $this->sub->register();
100 $this->sub->assertRegistered();
103 function testLocalPost()
105 $post = $this->pub->post("Local post, no subscribers yet.");
106 $this->assertNotEqual('', $post);
108 $post = $this->sub->post("Local post, no subscriptions yet.");
109 $this->assertNotEqual('', $post);
115 function testMentionUrl()
117 $bits = parse_url($this->b);
118 $base = $bits['host'];
119 if (isset($bits['path'])) {
120 $base .= $bits['path'];
122 $name = $this->sub->username;
124 $post = $this->pub->post("@$base/$name should have this in home and replies");
125 $this->sub->assertReceived($post);
128 function testSubscribe()
130 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
131 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
132 $this->sub->subscribe($this->pub->getProfileLink());
133 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
134 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
139 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
140 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
142 $name = $this->sub->username;
143 $post = $this->pub->post("Regular post, which $name should get via PuSH");
144 $this->sub->assertReceived($post);
147 function testMentionSubscribee()
149 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
150 $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri()));
152 $name = $this->pub->username;
153 $post = $this->sub->post("Just a quick note back to my remote subscribee @$name");
154 $this->pub->assertReceived($post);
157 function testUnsubscribe()
159 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
160 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
161 $this->sub->unsubscribe($this->pub->getProfileLink());
162 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
163 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
168 class SNTestClient extends TestBase
170 function __construct($base, $username, $password, $timeout=60)
172 $this->basepath = $base;
173 $this->username = $username;
174 $this->password = $password;
175 $this->timeout = $timeout;
177 $this->fullname = ucfirst($username) . ' Smith';
178 $this->homepage = 'http://example.org/' . $username;
179 $this->bio = 'Stub account for OStatus tests.';
180 $this->location = 'Montreal, QC';
184 * Make a low-level web hit to this site, with authentication.
185 * @param string $path URL fragment for something under the base path
186 * @param array $params POST parameters to send
187 * @param boolean $auth whether to include auth data
189 * @throws Exception on low-level error conditions
191 protected function hit($path, $params=array(), $auth=false, $cookies=array())
193 $url = $this->basepath . '/' . $path;
195 $http = new HTTP_Request2($url, 'POST', array('timeout' => $this->timeout));
197 $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC);
199 foreach ($cookies as $name => $val) {
200 $http->addCookie($name, $val);
202 $http->addPostParameter($params);
203 $response = $http->send();
205 $code = $response->getStatus();
206 if ($code < '200' || $code >= '400') {
207 throw new Exception("Failed API hit to $url: $code\n" . $response->getBody());
214 * Make a hit to a web form, without authentication but with a session.
215 * @param string $path URL fragment relative to site base
216 * @param string $form id of web form to pull initial parameters from
217 * @param array $params POST parameters, will be merged with defaults in form
219 protected function web($path, $form, $params=array())
221 $url = $this->basepath . '/' . $path;
222 $http = new HTTP_Request2($url, 'GET', array('timeout' => $this->timeout));
223 $response = $http->send();
225 $dom = $this->checkWeb($url, 'GET', $response);
227 foreach ($response->getCookies() as $cookie) {
228 // @fixme check for expirations etc
229 $cookies[$cookie['name']] = $cookie['value'];
232 $form = $dom->getElementById($form);
234 throw new Exception("Form $form not found on $url");
236 $inputs = $form->getElementsByTagName('input');
237 foreach ($inputs as $item) {
238 $type = $item->getAttribute('type');
239 if ($type != 'check') {
240 $name = $item->getAttribute('name');
241 $val = $item->getAttribute('value');
242 if ($name && $val && !isset($params[$name])) {
243 $params[$name] = $val;
248 $response = $this->hit($path, $params, false, $cookies);
249 $dom = $this->checkWeb($url, 'POST', $response);
254 protected function checkWeb($url, $method, $response)
256 $dom = new DOMDocument();
257 if (!$dom->loadHTML($response->getBody())) {
258 throw new Exception("Invalid HTML from $method to $url");
261 $xpath = new DOMXPath($dom);
262 $error = $xpath->query('//p[@class="error"]');
263 if ($error && $error->length) {
264 throw new Exception("Error on $method to $url: " .
265 $error->item(0)->textContent);
271 protected function parseXml($path, $body)
273 $dom = new DOMDocument();
274 if ($dom->loadXML($body)) {
277 throw new Exception("Bogus XML data from $path:\n$body");
282 * Make a hit to a REST-y XML page on the site, without authentication.
283 * @param string $path URL fragment for something relative to base
284 * @param array $params POST parameters to send
285 * @return DOMDocument
286 * @throws Exception on low-level error conditions
288 protected function xml($path, $params=array())
290 $response = $this->hit($path, $params, true);
291 $body = $response->getBody();
292 return $this->parseXml($path, $body);
295 protected function parseJson($path, $body)
297 $data = json_decode($body, true);
298 if ($data !== null) {
299 if (!empty($data['error'])) {
300 throw new Exception("JSON API returned error: " . $data['error']);
304 throw new Exception("Bogus JSON data from $path:\n$body");
309 * Make an API hit to this site, with authentication.
310 * @param string $path URL fragment for something under 'api' folder
311 * @param string $style one of 'json', 'xml', or 'atom'
312 * @param array $params POST parameters to send
313 * @return mixed associative array for JSON, DOMDocument for XML/Atom
314 * @throws Exception on low-level error conditions
316 protected function api($path, $style, $params=array())
318 $response = $this->hit("api/$path.$style", $params, true);
319 $body = $response->getBody();
320 if ($style == 'json') {
321 return $this->parseJson($path, $body);
322 } else if ($style == 'xml' || $style == 'atom') {
323 return $this->parseXml($path, $body);
325 throw new Exception("API needs to be JSON, XML, or Atom");
330 * Register the account.
332 * Unfortunately there's not an API method for registering, so we fake it.
336 $this->log("Registering user %s on %s",
339 $ret = $this->web('main/register', 'form_register',
340 array('nickname' => $this->username,
341 'password' => $this->password,
342 'confirm' => $this->password,
343 'fullname' => $this->fullname,
344 'homepage' => $this->homepage,
347 'submit' => 'Register'));
351 * @return string canonical URI/URL to profile page
353 function getProfileUri()
355 $data = $this->api('account/verify_credentials', 'json');
357 return $this->basepath . '/user/' . $id;
361 * @return string human-friendly URL to profile page
363 function getProfileLink()
365 return $this->basepath . '/' . $this->username;
369 * Check that the account has been registered and can be used.
370 * On failure, throws a test failure exception.
372 function assertRegistered()
374 $this->log("Confirming %s is registered on %s",
377 $data = $this->api('account/verify_credentials', 'json');
378 $this->assertEqual($this->username, $data['screen_name']);
379 $this->assertEqual($this->fullname, $data['name']);
380 $this->assertEqual($this->homepage, $data['url']);
381 $this->assertEqual($this->bio, $data['description']);
382 $this->log(" looks good!");
386 * Post a given message from this account
387 * @param string $message
388 * @return string URL/URI of notice
389 * @todo reply, location options
391 function post($message)
393 $this->log("Posting notice as %s on %s: %s",
397 $data = $this->api('statuses/update', 'json',
398 array('status' => $message));
400 $url = $this->basepath . '/notice/' . $data['id'];
405 * Check that this account has received the notice.
406 * @param string $notice_uri URI for the notice to check for
408 function assertReceived($notice_uri)
413 $ok = $this->checkReceived($notice_uri);
419 $this->log(" didn't see it yet, waiting $timeout seconds");
423 throw new Exception(" message $notice_uri not received by $this->username");
427 * Pull the user's home timeline to check if a notice with the given
428 * source URL has been received recently.
429 * If we don't see it, we'll try a couple more times up to 10 seconds.
431 * @param string $notice_uri
433 function checkReceived($notice_uri)
435 $this->log("Checking if %s on %s received notice %s",
440 $dom = $this->api('statuses/home_timeline', 'atom', $params);
442 $xml = simplexml_import_dom($dom);
446 if (is_array($xml->entry)) {
447 $entries = $xml->entry;
449 $entries = array($xml->entry);
451 foreach ($entries as $entry) {
452 if ($entry->id == $notice_uri) {
453 $this->log(" found it $notice_uri");
461 * @param string $profile user page link or webfinger
463 function subscribe($profile)
465 // This uses the command interface, since there's not currently
466 // a friendly Twit-API way to do a fresh remote subscription and
467 // the web form's a pain to use.
468 $this->post('follow ' . $profile);
472 * @param string $profile user page link or webfinger
474 function unsubscribe($profile)
476 // This uses the command interface, since there's not currently
477 // a friendly Twit-API way to do a fresh remote subscription and
478 // the web form's a pain to use.
479 $this->post('leave ' . $profile);
483 * Check that this account is subscribed to the given profile.
484 * @param string $profile_uri URI for the profile to check for
487 function hasSubscription($profile_uri)
489 $this->log("Checking if $this->username has a subscription to $profile_uri");
491 $me = $this->getProfileUri();
492 return $this->checkSubscription($me, $profile_uri);
496 * Check that this account is subscribed to by the given profile.
497 * @param string $profile_uri URI for the profile to check for
500 function hasSubscriber($profile_uri)
502 $this->log("Checking if $this->username is subscribed to by $profile_uri");
504 $me = $this->getProfileUri();
505 return $this->checkSubscription($profile_uri, $me);
508 protected function checkSubscription($subscriber, $subscribed)
510 // Using FOAF as the API methods for checking the social graph
511 // currently are unfriendly to remote profiles
512 $ns_foaf = 'http://xmlns.com/foaf/0.1/';
513 $ns_sioc = 'http://rdfs.org/sioc/ns#';
514 $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
516 $dom = $this->xml($this->username . '/foaf');
517 $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent');
518 foreach ($agents as $agent) {
519 $agent_uri = $agent->getAttributeNS($ns_rdf, 'about');
520 if ($agent_uri == $subscriber) {
521 $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows');
522 foreach ($follows as $follow) {
523 $target = $follow->getAttributeNS($ns_rdf, 'resource');
524 if ($target == ($subscribed . '#acct')) {
525 $this->log(" confirmed $subscriber subscribed to $subscribed");
529 $this->log(" we found $subscriber but they don't follow $subscribed");
533 $this->log(" can't find $subscriber in {$this->username}'s social graph.");
539 // @fixme switch to commandline.inc?
540 $timeout = HTTP_TIMEOUT;
544 foreach (array_slice($_SERVER['argv'], 1) as $arg) {
545 if (substr($arg, 0, 2) == '--') {
546 $bits = explode('=', substr($arg, 2), 2);
547 if (count($bits == 2)) {
548 list($key, $val) = $bits;
549 $options[$key] = $val;
552 $options[$key] = true;
558 if (count($args) < 2) {
560 remote-tests.php [options] <url1> <url2>
561 --timeout=## change HTTP timeout from default {$timeout}s
562 url1: base URL of a StatusNet instance
563 url2: base URL of another StatusNet instance
565 This will register user accounts on the two given StatusNet instances
566 and run some tests to confirm that OStatus subscription and posting
567 between the two sites works correctly.
575 if (isset($options['timeout'])) {
576 $timeout = intval($options['timeout']);
579 $tester = new OStatusTester($a, $b, $timeout);