3 if (php_sapi_name() != 'cli') {
7 define('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)
67 function __construct($a, $b) {
71 $base = 'test' . mt_rand(1, 1000000);
72 $this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000));
73 $this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000));
80 $methods = get_class_methods($this);
81 foreach ($methods as $method) {
82 if (strtolower(substr($method, 0, 4)) == 'test') {
84 print "== $method ==\n";
85 call_user_func(array($this, $method));
95 $this->pub->register();
96 $this->pub->assertRegistered();
98 $this->sub->register();
99 $this->sub->assertRegistered();
102 function testLocalPost()
104 $post = $this->pub->post("Local post, no subscribers yet.");
105 $this->assertNotEqual('', $post);
107 $post = $this->sub->post("Local post, no subscriptions yet.");
108 $this->assertNotEqual('', $post);
114 function testMentionUrl()
116 $bits = parse_url($this->b);
117 $base = $bits['host'];
118 if (isset($bits['path'])) {
119 $base .= $bits['path'];
121 $name = $this->sub->username;
123 $post = $this->pub->post("@$base/$name should have this in home and replies");
124 $this->sub->assertReceived($post);
127 function testSubscribe()
129 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
130 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
131 $this->sub->subscribe($this->pub->getProfileLink());
132 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
133 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
138 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
139 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
141 $name = $this->sub->username;
142 $post = $this->pub->post("Regular post, which $name should get via PuSH");
143 $this->sub->assertReceived($post);
146 function testMentionSubscribee()
148 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
149 $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri()));
151 $name = $this->pub->username;
152 $post = $this->sub->post("Just a quick note back to my remote subscribee @$name");
153 $this->pub->assertReceived($post);
156 function testUnsubscribe()
158 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
159 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
160 $this->sub->unsubscribe($this->pub->getProfileLink());
161 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
162 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
167 class SNTestClient extends TestBase
169 function __construct($base, $username, $password)
171 $this->basepath = $base;
172 $this->username = $username;
173 $this->password = $password;
175 $this->fullname = ucfirst($username) . ' Smith';
176 $this->homepage = 'http://example.org/' . $username;
177 $this->bio = 'Stub account for OStatus tests.';
178 $this->location = 'Montreal, QC';
182 * Make a low-level web hit to this site, with authentication.
183 * @param string $path URL fragment for something under the base path
184 * @param array $params POST parameters to send
185 * @param boolean $auth whether to include auth data
187 * @throws Exception on low-level error conditions
189 protected function hit($path, $params=array(), $auth=false, $cookies=array())
191 $url = $this->basepath . '/' . $path;
193 $http = new HTTP_Request2($url, 'POST', array('timeout' => TIMEOUT));
195 $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC);
197 foreach ($cookies as $name => $val) {
198 $http->addCookie($name, $val);
200 $http->addPostParameter($params);
201 $response = $http->send();
203 $code = $response->getStatus();
204 if ($code < '200' || $code >= '400') {
205 throw new Exception("Failed API hit to $url: $code\n" . $response->getBody());
212 * Make a hit to a web form, without authentication but with a session.
213 * @param string $path URL fragment relative to site base
214 * @param string $form id of web form to pull initial parameters from
215 * @param array $params POST parameters, will be merged with defaults in form
217 protected function web($path, $form, $params=array())
219 $url = $this->basepath . '/' . $path;
220 $http = new HTTP_Request2($url, 'GET', array('timeout' => TIMEOUT));
221 $response = $http->send();
223 $dom = $this->checkWeb($url, 'GET', $response);
225 foreach ($response->getCookies() as $cookie) {
226 // @fixme check for expirations etc
227 $cookies[$cookie['name']] = $cookie['value'];
230 $form = $dom->getElementById($form);
232 throw new Exception("Form $form not found on $url");
234 $inputs = $form->getElementsByTagName('input');
235 foreach ($inputs as $item) {
236 $type = $item->getAttribute('type');
237 if ($type != 'check') {
238 $name = $item->getAttribute('name');
239 $val = $item->getAttribute('value');
240 if ($name && $val && !isset($params[$name])) {
241 $params[$name] = $val;
246 $response = $this->hit($path, $params, false, $cookies);
247 $dom = $this->checkWeb($url, 'POST', $response);
252 protected function checkWeb($url, $method, $response)
254 $dom = new DOMDocument();
255 if (!$dom->loadHTML($response->getBody())) {
256 throw new Exception("Invalid HTML from $method to $url");
259 $xpath = new DOMXPath($dom);
260 $error = $xpath->query('//p[@class="error"]');
261 if ($error && $error->length) {
262 throw new Exception("Error on $method to $url: " .
263 $error->item(0)->textContent);
269 protected function parseXml($path, $body)
271 $dom = new DOMDocument();
272 if ($dom->loadXML($body)) {
275 throw new Exception("Bogus XML data from $path:\n$body");
280 * Make a hit to a REST-y XML page on the site, without authentication.
281 * @param string $path URL fragment for something relative to base
282 * @param array $params POST parameters to send
283 * @return DOMDocument
284 * @throws Exception on low-level error conditions
286 protected function xml($path, $params=array())
288 $response = $this->hit($path, $params, true);
289 $body = $response->getBody();
290 return $this->parseXml($path, $body);
293 protected function parseJson($path, $body)
295 $data = json_decode($body, true);
296 if ($data !== null) {
297 if (!empty($data['error'])) {
298 throw new Exception("JSON API returned error: " . $data['error']);
302 throw new Exception("Bogus JSON data from $path:\n$body");
307 * Make an API hit to this site, with authentication.
308 * @param string $path URL fragment for something under 'api' folder
309 * @param string $style one of 'json', 'xml', or 'atom'
310 * @param array $params POST parameters to send
311 * @return mixed associative array for JSON, DOMDocument for XML/Atom
312 * @throws Exception on low-level error conditions
314 protected function api($path, $style, $params=array())
316 $response = $this->hit("api/$path.$style", $params, true);
317 $body = $response->getBody();
318 if ($style == 'json') {
319 return $this->parseJson($path, $body);
320 } else if ($style == 'xml' || $style == 'atom') {
321 return $this->parseXml($path, $body);
323 throw new Exception("API needs to be JSON, XML, or Atom");
328 * Register the account.
330 * Unfortunately there's not an API method for registering, so we fake it.
334 $this->log("Registering user %s on %s",
337 $ret = $this->web('main/register', 'form_register',
338 array('nickname' => $this->username,
339 'password' => $this->password,
340 'confirm' => $this->password,
341 'fullname' => $this->fullname,
342 'homepage' => $this->homepage,
345 'submit' => 'Register'));
349 * @return string canonical URI/URL to profile page
351 function getProfileUri()
353 $data = $this->api('account/verify_credentials', 'json');
355 return $this->basepath . '/user/' . $id;
359 * @return string human-friendly URL to profile page
361 function getProfileLink()
363 return $this->basepath . '/' . $this->username;
367 * Check that the account has been registered and can be used.
368 * On failure, throws a test failure exception.
370 function assertRegistered()
372 $this->log("Confirming %s is registered on %s",
375 $data = $this->api('account/verify_credentials', 'json');
376 $this->assertEqual($this->username, $data['screen_name']);
377 $this->assertEqual($this->fullname, $data['name']);
378 $this->assertEqual($this->homepage, $data['url']);
379 $this->assertEqual($this->bio, $data['description']);
380 $this->log(" looks good!");
384 * Post a given message from this account
385 * @param string $message
386 * @return string URL/URI of notice
387 * @todo reply, location options
389 function post($message)
391 $this->log("Posting notice as %s on %s: %s",
395 $data = $this->api('statuses/update', 'json',
396 array('status' => $message));
398 $url = $this->basepath . '/notice/' . $data['id'];
403 * Check that this account has received the notice.
404 * @param string $notice_uri URI for the notice to check for
406 function assertReceived($notice_uri)
411 $ok = $this->checkReceived($notice_uri);
417 $this->log(" didn't see it yet, waiting $timeout seconds");
421 throw new Exception(" message $notice_uri not received by $this->username");
425 * Pull the user's home timeline to check if a notice with the given
426 * source URL has been received recently.
427 * If we don't see it, we'll try a couple more times up to 10 seconds.
429 * @param string $notice_uri
431 function checkReceived($notice_uri)
433 $this->log("Checking if %s on %s received notice %s",
438 $dom = $this->api('statuses/home_timeline', 'atom', $params);
440 $xml = simplexml_import_dom($dom);
444 if (is_array($xml->entry)) {
445 $entries = $xml->entry;
447 $entries = array($xml->entry);
449 foreach ($entries as $entry) {
450 if ($entry->id == $notice_uri) {
451 $this->log(" found it $notice_uri");
459 * @param string $profile user page link or webfinger
461 function subscribe($profile)
463 // This uses the command interface, since there's not currently
464 // a friendly Twit-API way to do a fresh remote subscription and
465 // the web form's a pain to use.
466 $this->post('follow ' . $profile);
470 * @param string $profile user page link or webfinger
472 function unsubscribe($profile)
474 // This uses the command interface, since there's not currently
475 // a friendly Twit-API way to do a fresh remote subscription and
476 // the web form's a pain to use.
477 $this->post('leave ' . $profile);
481 * Check that this account is subscribed to the given profile.
482 * @param string $profile_uri URI for the profile to check for
485 function hasSubscription($profile_uri)
487 $this->log("Checking if $this->username has a subscription to $profile_uri");
489 $me = $this->getProfileUri();
490 return $this->checkSubscription($me, $profile_uri);
494 * Check that this account is subscribed to by the given profile.
495 * @param string $profile_uri URI for the profile to check for
498 function hasSubscriber($profile_uri)
500 $this->log("Checking if $this->username is subscribed to by $profile_uri");
502 $me = $this->getProfileUri();
503 return $this->checkSubscription($profile_uri, $me);
506 protected function checkSubscription($subscriber, $subscribed)
508 // Using FOAF as the API methods for checking the social graph
509 // currently are unfriendly to remote profiles
510 $ns_foaf = 'http://xmlns.com/foaf/0.1/';
511 $ns_sioc = 'http://rdfs.org/sioc/ns#';
512 $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
514 $dom = $this->xml($this->username . '/foaf');
515 $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent');
516 foreach ($agents as $agent) {
517 $agent_uri = $agent->getAttributeNS($ns_rdf, 'about');
518 if ($agent_uri == $subscriber) {
519 $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows');
520 foreach ($follows as $follow) {
521 $target = $follow->getAttributeNS($ns_rdf, 'resource');
522 if ($target == ($subscribed . '#acct')) {
523 $this->log(" confirmed $subscriber subscribed to $subscribed");
527 $this->log(" we found $subscriber but they don't follow $subscribed");
531 $this->log(" can't find $subscriber in {$this->username}'s social graph.");
537 $args = array_slice($_SERVER['argv'], 1);
538 if (count($args) < 2) {
540 remote-tests.php <url1> <url2>
541 url1: base URL of a StatusNet instance
542 url2: base URL of another StatusNet instance
544 This will register user accounts on the two given StatusNet instances
545 and run some tests to confirm that OStatus subscription and posting
546 between the two sites works correctly.
555 $tester = new OStatusTester($a, $b);