3 if (php_sapi_name() != 'cli') {
7 define('INSTALLDIR', dirname(dirname(dirname(dirname(__FILE__)))));
8 set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path());
10 require_once 'PEAR.php';
11 require_once 'Net/URL2.php';
12 require_once 'HTTP/Request2.php';
15 // ostatus test script, client-side :)
21 $args = func_get_args();
24 $msg = vsprintf($str, $args);
28 function assertEqual($a, $b)
31 throw new Exception("Failed to assert equality: expected $a, got $b");
36 function assertNotEqual($a, $b)
39 throw new Exception("Failed to assert inequality: expected not $a, got $b");
44 function assertTrue($a)
47 throw new Exception("Failed to assert true: got false");
51 function assertFalse($a)
54 throw new Exception("Failed to assert false: got true");
59 class OStatusTester extends TestBase
62 * @param string $a base URL of test site A (eg http://localhost/mublog)
63 * @param string $b base URL of test site B (eg http://localhost/mublog2)
65 function __construct($a, $b) {
69 $base = 'test' . mt_rand(1, 1000000);
70 $this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000));
71 $this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000));
78 $this->testLocalPost();
79 $this->testMentionUrl();
80 $this->testSubscribe();
82 $this->testMentionSubscribee();
83 $this->testUnsubscribe();
90 $this->pub->register();
91 $this->pub->assertRegistered();
93 $this->sub->register();
94 $this->sub->assertRegistered();
97 function testLocalPost()
99 $post = $this->pub->post("Local post, no subscribers yet.");
100 $this->assertNotEqual('', $post);
102 $post = $this->sub->post("Local post, no subscriptions yet.");
103 $this->assertNotEqual('', $post);
109 function testMentionUrl()
111 $bits = parse_url($this->b);
112 $base = $bits['host'];
113 if (isset($bits['path'])) {
114 $base .= $bits['path'];
116 $name = $this->sub->username;
118 $post = $this->pub->post("@$base/$name should have this in home and replies");
119 $this->sub->assertReceived($post);
122 function testSubscribe()
124 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
125 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
126 $this->sub->subscribe($this->pub->getProfileLink());
127 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
128 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
133 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
134 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
136 $name = $this->sub->username;
137 $post = $this->pub->post("Regular post, which $name should get via PuSH");
138 $this->sub->assertReceived($post);
141 function testMentionSubscribee()
143 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
144 $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri()));
146 $name = $this->pub->username;
147 $post = $this->sub->post("Just a quick note back to my remote subscribee @$name");
148 $this->pub->assertReceived($post);
151 function testUnsubscribe()
153 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
154 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
155 $this->sub->unsubscribe($this->pub->getProfileLink());
156 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
157 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
162 class SNTestClient extends TestBase
164 function __construct($base, $username, $password)
166 $this->basepath = $base;
167 $this->username = $username;
168 $this->password = $password;
170 $this->fullname = ucfirst($username) . ' Smith';
171 $this->homepage = 'http://example.org/' . $username;
172 $this->bio = 'Stub account for OStatus tests.';
173 $this->location = 'Montreal, QC';
177 * Make a low-level web hit to this site, with authentication.
178 * @param string $path URL fragment for something under the base path
179 * @param array $params POST parameters to send
180 * @param boolean $auth whether to include auth data
182 * @throws Exception on low-level error conditions
184 protected function hit($path, $params=array(), $auth=false, $cookies=array())
186 $url = $this->basepath . '/' . $path;
188 $http = new HTTP_Request2($url, 'POST');
190 $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC);
192 foreach ($cookies as $name => $val) {
193 $http->addCookie($name, $val);
195 $http->addPostParameter($params);
196 $response = $http->send();
198 $code = $response->getStatus();
199 if ($code < '200' || $code >= '400') {
200 throw new Exception("Failed API hit to $url: $code\n" . $response->getBody());
207 * Make a hit to a web form, without authentication but with a session.
208 * @param string $path URL fragment relative to site base
209 * @param string $form id of web form to pull initial parameters from
210 * @param array $params POST parameters, will be merged with defaults in form
212 protected function web($path, $form, $params=array())
214 $url = $this->basepath . '/' . $path;
215 $http = new HTTP_Request2($url, 'GET');
216 $response = $http->send();
218 $dom = $this->checkWeb($url, 'GET', $response);
220 foreach ($response->getCookies() as $cookie) {
221 // @fixme check for expirations etc
222 $cookies[$cookie['name']] = $cookie['value'];
225 $form = $dom->getElementById($form);
227 throw new Exception("Form $form not found on $url");
229 $inputs = $form->getElementsByTagName('input');
230 foreach ($inputs as $item) {
231 $type = $item->getAttribute('type');
232 if ($type != 'check') {
233 $name = $item->getAttribute('name');
234 $val = $item->getAttribute('value');
235 if ($name && $val && !isset($params[$name])) {
236 $params[$name] = $val;
241 $response = $this->hit($path, $params, false, $cookies);
242 $dom = $this->checkWeb($url, 'POST', $response);
247 protected function checkWeb($url, $method, $response)
249 $dom = new DOMDocument();
250 if (!$dom->loadHTML($response->getBody())) {
251 throw new Exception("Invalid HTML from $method to $url");
254 $xpath = new DOMXPath($dom);
255 $error = $xpath->query('//p[@class="error"]');
256 if ($error && $error->length) {
257 throw new Exception("Error on $method to $url: " .
258 $error->item(0)->textContent);
264 protected function parseXml($path, $body)
266 $dom = new DOMDocument();
267 if ($dom->loadXML($body)) {
270 throw new Exception("Bogus XML data from $path:\n$body");
275 * Make a hit to a REST-y XML page on the site, without authentication.
276 * @param string $path URL fragment for something relative to base
277 * @param array $params POST parameters to send
278 * @return DOMDocument
279 * @throws Exception on low-level error conditions
281 protected function xml($path, $params=array())
283 $response = $this->hit($path, $params, true);
284 $body = $response->getBody();
285 return $this->parseXml($path, $body);
288 protected function parseJson($path, $body)
290 $data = json_decode($body, true);
291 if ($data !== null) {
292 if (!empty($data['error'])) {
293 throw new Exception("JSON API returned error: " . $data['error']);
297 throw new Exception("Bogus JSON data from $path:\n$body");
302 * Make an API hit to this site, with authentication.
303 * @param string $path URL fragment for something under 'api' folder
304 * @param string $style one of 'json', 'xml', or 'atom'
305 * @param array $params POST parameters to send
306 * @return mixed associative array for JSON, DOMDocument for XML/Atom
307 * @throws Exception on low-level error conditions
309 protected function api($path, $style, $params=array())
311 $response = $this->hit("api/$path.$style", $params, true);
312 $body = $response->getBody();
313 if ($style == 'json') {
314 return $this->parseJson($path, $body);
315 } else if ($style == 'xml' || $style == 'atom') {
316 return $this->parseXml($path, $body);
318 throw new Exception("API needs to be JSON, XML, or Atom");
323 * Register the account.
325 * Unfortunately there's not an API method for registering, so we fake it.
329 $this->log("Registering user %s on %s",
332 $ret = $this->web('main/register', 'form_register',
333 array('nickname' => $this->username,
334 'password' => $this->password,
335 'confirm' => $this->password,
336 'fullname' => $this->fullname,
337 'homepage' => $this->homepage,
340 'submit' => 'Register'));
344 * @return string canonical URI/URL to profile page
346 function getProfileUri()
348 $data = $this->api('account/verify_credentials', 'json');
350 return $this->basepath . '/user/' . $id;
354 * @return string human-friendly URL to profile page
356 function getProfileLink()
358 return $this->basepath . '/' . $this->username;
362 * Check that the account has been registered and can be used.
363 * On failure, throws a test failure exception.
365 function assertRegistered()
367 $this->log("Confirming %s is registered on %s",
370 $data = $this->api('account/verify_credentials', 'json');
371 $this->assertEqual($this->username, $data['screen_name']);
372 $this->assertEqual($this->fullname, $data['name']);
373 $this->assertEqual($this->homepage, $data['url']);
374 $this->assertEqual($this->bio, $data['description']);
378 * Post a given message from this account
379 * @param string $message
380 * @return string URL/URI of notice
381 * @todo reply, location options
383 function post($message)
385 $this->log("Posting notice as %s on %s: %s",
389 $data = $this->api('statuses/update', 'json',
390 array('status' => $message));
392 $url = $this->basepath . '/notice/' . $data['id'];
397 * Check that this account has received the notice.
398 * @param string $notice_uri URI for the notice to check for
400 function assertReceived($notice_uri)
405 $ok = $this->checkReceived($notice_uri);
411 $this->log("Didn't see it yet, waiting $timeout seconds");
415 throw new Exception("Message $notice_uri not received by $this->username");
419 * Pull the user's home timeline to check if a notice with the given
420 * source URL has been received recently.
421 * If we don't see it, we'll try a couple more times up to 10 seconds.
423 * @param string $notice_uri
425 function checkReceived($notice_uri)
427 $this->log("Checking if %s on %s received notice %s",
432 $dom = $this->api('statuses/home_timeline', 'atom', $params);
434 $xml = simplexml_import_dom($dom);
438 if (is_array($xml->entry)) {
439 $entries = $xml->entry;
441 $entries = array($xml->entry);
443 foreach ($entries as $entry) {
444 if ($entry->id == $notice_uri) {
445 $this->log("found it $notice_uri");
448 //$this->log("nope... " . $entry->id);
454 * @param string $profile user page link or webfinger
456 function subscribe($profile)
458 // This uses the command interface, since there's not currently
459 // a friendly Twit-API way to do a fresh remote subscription and
460 // the web form's a pain to use.
461 $this->post('follow ' . $profile);
465 * @param string $profile user page link or webfinger
467 function unsubscribe($profile)
469 // This uses the command interface, since there's not currently
470 // a friendly Twit-API way to do a fresh remote subscription and
471 // the web form's a pain to use.
472 $this->post('leave ' . $profile);
476 * Check that this account is subscribed to the given profile.
477 * @param string $profile_uri URI for the profile to check for
480 function hasSubscription($profile_uri)
482 $this->log("Checking if $this->username has a subscription to $profile_uri");
484 $me = $this->getProfileUri();
485 return $this->checkSubscription($me, $profile_uri);
489 * Check that this account is subscribed to by the given profile.
490 * @param string $profile_uri URI for the profile to check for
493 function hasSubscriber($profile_uri)
495 $this->log("Checking if $this->username is subscribed to by $profile_uri");
497 $me = $this->getProfileUri();
498 return $this->checkSubscription($profile_uri, $me);
501 protected function checkSubscription($subscriber, $subscribed)
503 // Using FOAF as the API methods for checking the social graph
504 // currently are unfriendly to remote profiles
505 $ns_foaf = 'http://xmlns.com/foaf/0.1/';
506 $ns_sioc = 'http://rdfs.org/sioc/ns#';
507 $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
509 $dom = $this->xml($this->username . '/foaf');
510 $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent');
511 foreach ($agents as $agent) {
512 $agent_uri = $agent->getAttributeNS($ns_rdf, 'about');
513 if ($agent_uri == $subscriber) {
514 $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows');
515 foreach ($follows as $follow) {
516 $target = $follow->getAttributeNS($ns_rdf, 'resource');
517 if ($target == ($subscribed . '#acct')) {
518 $this->log("Confirmed $subscriber subscribed to $subscribed");
522 $this->log("We found $subscriber but they don't follow $subscribed");
526 $this->log("Can't find $subscriber in {$this->username}'s social graph.");
532 $args = array_slice($_SERVER['argv'], 1);
533 if (count($args) < 2) {
535 remote-tests.php <url1> <url2>
536 url1: base URL of a StatusNet instance
537 url2: base URL of another StatusNet instance
539 This will register user accounts on the two given StatusNet instances
540 and run some tests to confirm that OStatus subscription and posting
541 between the two sites works correctly.
550 $tester = new OStatusTester($a, $b);