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 $methods = get_class_methods($this);
79 foreach ($methods as $method) {
80 if (strtolower(substr($method, 0, 4)) == 'test') {
82 print "== $method ==\n";
83 call_user_func(array($this, $method));
93 $this->pub->register();
94 $this->pub->assertRegistered();
96 $this->sub->register();
97 $this->sub->assertRegistered();
100 function testLocalPost()
102 $post = $this->pub->post("Local post, no subscribers yet.");
103 $this->assertNotEqual('', $post);
105 $post = $this->sub->post("Local post, no subscriptions yet.");
106 $this->assertNotEqual('', $post);
112 function testMentionUrl()
114 $bits = parse_url($this->b);
115 $base = $bits['host'];
116 if (isset($bits['path'])) {
117 $base .= $bits['path'];
119 $name = $this->sub->username;
121 $post = $this->pub->post("@$base/$name should have this in home and replies");
122 $this->sub->assertReceived($post);
125 function testSubscribe()
127 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
128 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
129 $this->sub->subscribe($this->pub->getProfileLink());
130 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
131 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
136 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
137 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
139 $name = $this->sub->username;
140 $post = $this->pub->post("Regular post, which $name should get via PuSH");
141 $this->sub->assertReceived($post);
144 function testMentionSubscribee()
146 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
147 $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri()));
149 $name = $this->pub->username;
150 $post = $this->sub->post("Just a quick note back to my remote subscribee @$name");
151 $this->pub->assertReceived($post);
154 function testUnsubscribe()
156 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
157 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
158 $this->sub->unsubscribe($this->pub->getProfileLink());
159 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
160 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
165 class SNTestClient extends TestBase
167 function __construct($base, $username, $password)
169 $this->basepath = $base;
170 $this->username = $username;
171 $this->password = $password;
173 $this->fullname = ucfirst($username) . ' Smith';
174 $this->homepage = 'http://example.org/' . $username;
175 $this->bio = 'Stub account for OStatus tests.';
176 $this->location = 'Montreal, QC';
180 * Make a low-level web hit to this site, with authentication.
181 * @param string $path URL fragment for something under the base path
182 * @param array $params POST parameters to send
183 * @param boolean $auth whether to include auth data
185 * @throws Exception on low-level error conditions
187 protected function hit($path, $params=array(), $auth=false, $cookies=array())
189 $url = $this->basepath . '/' . $path;
191 $http = new HTTP_Request2($url, 'POST');
193 $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC);
195 foreach ($cookies as $name => $val) {
196 $http->addCookie($name, $val);
198 $http->addPostParameter($params);
199 $response = $http->send();
201 $code = $response->getStatus();
202 if ($code < '200' || $code >= '400') {
203 throw new Exception("Failed API hit to $url: $code\n" . $response->getBody());
210 * Make a hit to a web form, without authentication but with a session.
211 * @param string $path URL fragment relative to site base
212 * @param string $form id of web form to pull initial parameters from
213 * @param array $params POST parameters, will be merged with defaults in form
215 protected function web($path, $form, $params=array())
217 $url = $this->basepath . '/' . $path;
218 $http = new HTTP_Request2($url, 'GET');
219 $response = $http->send();
221 $dom = $this->checkWeb($url, 'GET', $response);
223 foreach ($response->getCookies() as $cookie) {
224 // @fixme check for expirations etc
225 $cookies[$cookie['name']] = $cookie['value'];
228 $form = $dom->getElementById($form);
230 throw new Exception("Form $form not found on $url");
232 $inputs = $form->getElementsByTagName('input');
233 foreach ($inputs as $item) {
234 $type = $item->getAttribute('type');
235 if ($type != 'check') {
236 $name = $item->getAttribute('name');
237 $val = $item->getAttribute('value');
238 if ($name && $val && !isset($params[$name])) {
239 $params[$name] = $val;
244 $response = $this->hit($path, $params, false, $cookies);
245 $dom = $this->checkWeb($url, 'POST', $response);
250 protected function checkWeb($url, $method, $response)
252 $dom = new DOMDocument();
253 if (!$dom->loadHTML($response->getBody())) {
254 throw new Exception("Invalid HTML from $method to $url");
257 $xpath = new DOMXPath($dom);
258 $error = $xpath->query('//p[@class="error"]');
259 if ($error && $error->length) {
260 throw new Exception("Error on $method to $url: " .
261 $error->item(0)->textContent);
267 protected function parseXml($path, $body)
269 $dom = new DOMDocument();
270 if ($dom->loadXML($body)) {
273 throw new Exception("Bogus XML data from $path:\n$body");
278 * Make a hit to a REST-y XML page on the site, without authentication.
279 * @param string $path URL fragment for something relative to base
280 * @param array $params POST parameters to send
281 * @return DOMDocument
282 * @throws Exception on low-level error conditions
284 protected function xml($path, $params=array())
286 $response = $this->hit($path, $params, true);
287 $body = $response->getBody();
288 return $this->parseXml($path, $body);
291 protected function parseJson($path, $body)
293 $data = json_decode($body, true);
294 if ($data !== null) {
295 if (!empty($data['error'])) {
296 throw new Exception("JSON API returned error: " . $data['error']);
300 throw new Exception("Bogus JSON data from $path:\n$body");
305 * Make an API hit to this site, with authentication.
306 * @param string $path URL fragment for something under 'api' folder
307 * @param string $style one of 'json', 'xml', or 'atom'
308 * @param array $params POST parameters to send
309 * @return mixed associative array for JSON, DOMDocument for XML/Atom
310 * @throws Exception on low-level error conditions
312 protected function api($path, $style, $params=array())
314 $response = $this->hit("api/$path.$style", $params, true);
315 $body = $response->getBody();
316 if ($style == 'json') {
317 return $this->parseJson($path, $body);
318 } else if ($style == 'xml' || $style == 'atom') {
319 return $this->parseXml($path, $body);
321 throw new Exception("API needs to be JSON, XML, or Atom");
326 * Register the account.
328 * Unfortunately there's not an API method for registering, so we fake it.
332 $this->log("Registering user %s on %s",
335 $ret = $this->web('main/register', 'form_register',
336 array('nickname' => $this->username,
337 'password' => $this->password,
338 'confirm' => $this->password,
339 'fullname' => $this->fullname,
340 'homepage' => $this->homepage,
343 'submit' => 'Register'));
347 * @return string canonical URI/URL to profile page
349 function getProfileUri()
351 $data = $this->api('account/verify_credentials', 'json');
353 return $this->basepath . '/user/' . $id;
357 * @return string human-friendly URL to profile page
359 function getProfileLink()
361 return $this->basepath . '/' . $this->username;
365 * Check that the account has been registered and can be used.
366 * On failure, throws a test failure exception.
368 function assertRegistered()
370 $this->log("Confirming %s is registered on %s",
373 $data = $this->api('account/verify_credentials', 'json');
374 $this->assertEqual($this->username, $data['screen_name']);
375 $this->assertEqual($this->fullname, $data['name']);
376 $this->assertEqual($this->homepage, $data['url']);
377 $this->assertEqual($this->bio, $data['description']);
378 $this->log(" looks good!");
382 * Post a given message from this account
383 * @param string $message
384 * @return string URL/URI of notice
385 * @todo reply, location options
387 function post($message)
389 $this->log("Posting notice as %s on %s: %s",
393 $data = $this->api('statuses/update', 'json',
394 array('status' => $message));
396 $url = $this->basepath . '/notice/' . $data['id'];
401 * Check that this account has received the notice.
402 * @param string $notice_uri URI for the notice to check for
404 function assertReceived($notice_uri)
409 $ok = $this->checkReceived($notice_uri);
415 $this->log(" didn't see it yet, waiting $timeout seconds");
419 throw new Exception(" message $notice_uri not received by $this->username");
423 * Pull the user's home timeline to check if a notice with the given
424 * source URL has been received recently.
425 * If we don't see it, we'll try a couple more times up to 10 seconds.
427 * @param string $notice_uri
429 function checkReceived($notice_uri)
431 $this->log("Checking if %s on %s received notice %s",
436 $dom = $this->api('statuses/home_timeline', 'atom', $params);
438 $xml = simplexml_import_dom($dom);
442 if (is_array($xml->entry)) {
443 $entries = $xml->entry;
445 $entries = array($xml->entry);
447 foreach ($entries as $entry) {
448 if ($entry->id == $notice_uri) {
449 $this->log(" found it $notice_uri");
457 * @param string $profile user page link or webfinger
459 function subscribe($profile)
461 // This uses the command interface, since there's not currently
462 // a friendly Twit-API way to do a fresh remote subscription and
463 // the web form's a pain to use.
464 $this->post('follow ' . $profile);
468 * @param string $profile user page link or webfinger
470 function unsubscribe($profile)
472 // This uses the command interface, since there's not currently
473 // a friendly Twit-API way to do a fresh remote subscription and
474 // the web form's a pain to use.
475 $this->post('leave ' . $profile);
479 * Check that this account is subscribed to the given profile.
480 * @param string $profile_uri URI for the profile to check for
483 function hasSubscription($profile_uri)
485 $this->log("Checking if $this->username has a subscription to $profile_uri");
487 $me = $this->getProfileUri();
488 return $this->checkSubscription($me, $profile_uri);
492 * Check that this account is subscribed to by the given profile.
493 * @param string $profile_uri URI for the profile to check for
496 function hasSubscriber($profile_uri)
498 $this->log("Checking if $this->username is subscribed to by $profile_uri");
500 $me = $this->getProfileUri();
501 return $this->checkSubscription($profile_uri, $me);
504 protected function checkSubscription($subscriber, $subscribed)
506 // Using FOAF as the API methods for checking the social graph
507 // currently are unfriendly to remote profiles
508 $ns_foaf = 'http://xmlns.com/foaf/0.1/';
509 $ns_sioc = 'http://rdfs.org/sioc/ns#';
510 $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
512 $dom = $this->xml($this->username . '/foaf');
513 $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent');
514 foreach ($agents as $agent) {
515 $agent_uri = $agent->getAttributeNS($ns_rdf, 'about');
516 if ($agent_uri == $subscriber) {
517 $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows');
518 foreach ($follows as $follow) {
519 $target = $follow->getAttributeNS($ns_rdf, 'resource');
520 if ($target == ($subscribed . '#acct')) {
521 $this->log(" confirmed $subscriber subscribed to $subscribed");
525 $this->log(" we found $subscriber but they don't follow $subscribed");
529 $this->log(" can't find $subscriber in {$this->username}'s social graph.");
535 $args = array_slice($_SERVER['argv'], 1);
536 if (count($args) < 2) {
538 remote-tests.php <url1> <url2>
539 url1: base URL of a StatusNet instance
540 url2: base URL of another StatusNet instance
542 This will register user accounts on the two given StatusNet instances
543 and run some tests to confirm that OStatus subscription and posting
544 between the two sites works correctly.
553 $tester = new OStatusTester($a, $b);