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();
81 $this->testUnsubscribe();
88 $this->pub->register();
89 $this->pub->assertRegistered();
91 $this->sub->register();
92 $this->sub->assertRegistered();
95 function testLocalPost()
97 $post = $this->pub->post("Local post, no subscribers yet.");
98 $this->assertNotEqual('', $post);
100 $post = $this->sub->post("Local post, no subscriptions yet.");
101 $this->assertNotEqual('', $post);
107 function testMentionUrl()
109 $bits = parse_url($this->b);
110 $base = $bits['host'];
111 if (isset($bits['path'])) {
112 $base .= $bits['path'];
114 $name = $this->sub->username;
116 $post = $this->pub->post("@$base/$name should have this in home and replies");
117 $this->sub->assertReceived($post);
120 function testSubscribe()
122 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
123 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
124 $this->sub->subscribe($this->pub->getProfileLink());
125 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
126 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
129 function testUnsubscribe()
131 $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
132 $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
133 $this->sub->unsubscribe($this->pub->getProfileLink());
134 $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
135 $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
140 class SNTestClient extends TestBase
142 function __construct($base, $username, $password)
144 $this->basepath = $base;
145 $this->username = $username;
146 $this->password = $password;
148 $this->fullname = ucfirst($username) . ' Smith';
149 $this->homepage = 'http://example.org/' . $username;
150 $this->bio = 'Stub account for OStatus tests.';
151 $this->location = 'Montreal, QC';
155 * Make a low-level web hit to this site, with authentication.
156 * @param string $path URL fragment for something under the base path
157 * @param array $params POST parameters to send
158 * @param boolean $auth whether to include auth data
160 * @throws Exception on low-level error conditions
162 protected function hit($path, $params=array(), $auth=false, $cookies=array())
164 $url = $this->basepath . '/' . $path;
166 $http = new HTTP_Request2($url, 'POST');
168 $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC);
170 foreach ($cookies as $name => $val) {
171 $http->addCookie($name, $val);
173 $http->addPostParameter($params);
174 $response = $http->send();
176 $code = $response->getStatus();
177 if ($code < '200' || $code >= '400') {
178 throw new Exception("Failed API hit to $url: $code\n" . $response->getBody());
185 * Make a hit to a web form, without authentication but with a session.
186 * @param string $path URL fragment relative to site base
187 * @param string $form id of web form to pull initial parameters from
188 * @param array $params POST parameters, will be merged with defaults in form
190 protected function web($path, $form, $params=array())
192 $url = $this->basepath . '/' . $path;
193 $http = new HTTP_Request2($url, 'GET');
194 $response = $http->send();
196 $dom = $this->checkWeb($url, 'GET', $response);
198 foreach ($response->getCookies() as $cookie) {
199 // @fixme check for expirations etc
200 $cookies[$cookie['name']] = $cookie['value'];
203 $form = $dom->getElementById($form);
205 throw new Exception("Form $form not found on $url");
207 $inputs = $form->getElementsByTagName('input');
208 foreach ($inputs as $item) {
209 $type = $item->getAttribute('type');
210 if ($type != 'check') {
211 $name = $item->getAttribute('name');
212 $val = $item->getAttribute('value');
213 if ($name && $val && !isset($params[$name])) {
214 $params[$name] = $val;
219 $response = $this->hit($path, $params, false, $cookies);
220 $dom = $this->checkWeb($url, 'POST', $response);
225 protected function checkWeb($url, $method, $response)
227 $dom = new DOMDocument();
228 if (!$dom->loadHTML($response->getBody())) {
229 throw new Exception("Invalid HTML from $method to $url");
232 $xpath = new DOMXPath($dom);
233 $error = $xpath->query('//p[@class="error"]');
234 if ($error && $error->length) {
235 throw new Exception("Error on $method to $url: " .
236 $error->item(0)->textContent);
242 protected function parseXml($path, $body)
244 $dom = new DOMDocument();
245 if ($dom->loadXML($body)) {
248 throw new Exception("Bogus XML data from $path:\n$body");
253 * Make a hit to a REST-y XML page on the site, without authentication.
254 * @param string $path URL fragment for something relative to base
255 * @param array $params POST parameters to send
256 * @return DOMDocument
257 * @throws Exception on low-level error conditions
259 protected function xml($path, $params=array())
261 $response = $this->hit($path, $params, true);
262 $body = $response->getBody();
263 return $this->parseXml($path, $body);
266 protected function parseJson($path, $body)
268 $data = json_decode($body, true);
269 if ($data !== null) {
270 if (!empty($data['error'])) {
271 throw new Exception("JSON API returned error: " . $data['error']);
275 throw new Exception("Bogus JSON data from $path:\n$body");
280 * Make an API hit to this site, with authentication.
281 * @param string $path URL fragment for something under 'api' folder
282 * @param string $style one of 'json', 'xml', or 'atom'
283 * @param array $params POST parameters to send
284 * @return mixed associative array for JSON, DOMDocument for XML/Atom
285 * @throws Exception on low-level error conditions
287 protected function api($path, $style, $params=array())
289 $response = $this->hit("api/$path.$style", $params, true);
290 $body = $response->getBody();
291 if ($style == 'json') {
292 return $this->parseJson($path, $body);
293 } else if ($style == 'xml' || $style == 'atom') {
294 return $this->parseXml($path, $body);
296 throw new Exception("API needs to be JSON, XML, or Atom");
301 * Register the account.
303 * Unfortunately there's not an API method for registering, so we fake it.
307 $this->log("Registering user %s on %s",
310 $ret = $this->web('main/register', 'form_register',
311 array('nickname' => $this->username,
312 'password' => $this->password,
313 'confirm' => $this->password,
314 'fullname' => $this->fullname,
315 'homepage' => $this->homepage,
318 'submit' => 'Register'));
322 * @return string canonical URI/URL to profile page
324 function getProfileUri()
326 $data = $this->api('account/verify_credentials', 'json');
328 return $this->basepath . '/user/' . $id;
332 * @return string human-friendly URL to profile page
334 function getProfileLink()
336 return $this->basepath . '/' . $this->username;
340 * Check that the account has been registered and can be used.
341 * On failure, throws a test failure exception.
343 function assertRegistered()
345 $this->log("Confirming %s is registered on %s",
348 $data = $this->api('account/verify_credentials', 'json');
349 $this->assertEqual($this->username, $data['screen_name']);
350 $this->assertEqual($this->fullname, $data['name']);
351 $this->assertEqual($this->homepage, $data['url']);
352 $this->assertEqual($this->bio, $data['description']);
356 * Post a given message from this account
357 * @param string $message
358 * @return string URL/URI of notice
359 * @todo reply, location options
361 function post($message)
363 $this->log("Posting notice as %s on %s: %s",
367 $data = $this->api('statuses/update', 'json',
368 array('status' => $message));
370 $url = $this->basepath . '/notice/' . $data['id'];
375 * Check that this account has received the notice.
376 * @param string $notice_uri URI for the notice to check for
378 function assertReceived($notice_uri)
383 $ok = $this->checkReceived($notice_uri);
389 $this->log("Didn't see it yet, waiting $timeout seconds");
393 throw new Exception("Message $notice_uri not received by $this->username");
397 * Pull the user's home timeline to check if a notice with the given
398 * source URL has been received recently.
399 * If we don't see it, we'll try a couple more times up to 10 seconds.
401 * @param string $notice_uri
403 function checkReceived($notice_uri)
405 $this->log("Checking if %s on %s received notice %s",
410 $dom = $this->api('statuses/home_timeline', 'atom', $params);
412 $xml = simplexml_import_dom($dom);
416 if (is_array($xml->entry)) {
417 $entries = $xml->entry;
419 $entries = array($xml->entry);
421 foreach ($entries as $entry) {
422 if ($entry->id == $notice_uri) {
423 $this->log("found it $notice_uri");
426 //$this->log("nope... " . $entry->id);
432 * @param string $profile user page link or webfinger
434 function subscribe($profile)
436 // This uses the command interface, since there's not currently
437 // a friendly Twit-API way to do a fresh remote subscription and
438 // the web form's a pain to use.
439 $this->post('follow ' . $profile);
443 * @param string $profile user page link or webfinger
445 function unsubscribe($profile)
447 // This uses the command interface, since there's not currently
448 // a friendly Twit-API way to do a fresh remote subscription and
449 // the web form's a pain to use.
450 $this->post('leave ' . $profile);
454 * Check that this account is subscribed to the given profile.
455 * @param string $profile_uri URI for the profile to check for
458 function hasSubscription($profile_uri)
460 $this->log("Checking if $this->username has a subscription to $profile_uri");
462 $me = $this->getProfileUri();
463 return $this->checkSubscription($me, $profile_uri);
467 * Check that this account is subscribed to by the given profile.
468 * @param string $profile_uri URI for the profile to check for
471 function hasSubscriber($profile_uri)
473 $this->log("Checking if $this->username is subscribed to by $profile_uri");
475 $me = $this->getProfileUri();
476 return $this->checkSubscription($profile_uri, $me);
479 protected function checkSubscription($subscriber, $subscribed)
481 // Using FOAF as the API methods for checking the social graph
482 // currently are unfriendly to remote profiles
483 $ns_foaf = 'http://xmlns.com/foaf/0.1/';
484 $ns_sioc = 'http://rdfs.org/sioc/ns#';
485 $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
487 $dom = $this->xml($this->username . '/foaf');
488 $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent');
489 foreach ($agents as $agent) {
490 $agent_uri = $agent->getAttributeNS($ns_rdf, 'about');
491 if ($agent_uri == $subscriber) {
492 $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows');
493 foreach ($follows as $follow) {
494 $target = $follow->getAttributeNS($ns_rdf, 'resource');
495 if ($target == ($subscribed . '#acct')) {
496 $this->log("Confirmed $subscriber subscribed to $subscribed");
500 $this->log("We found $subscriber but they don't follow $subscribed");
504 $this->log("Can't find $subscriber in {$this->username}'s social graph.");
510 $args = array_slice($_SERVER['argv'], 1);
511 if (count($args) < 2) {
513 remote-tests.php <url1> <url2>
514 url1: base URL of a StatusNet instance
515 url2: base URL of another StatusNet instance
517 This will register user accounts on the two given StatusNet instances
518 and run some tests to confirm that OStatus subscription and posting
519 between the two sites works correctly.
528 $tester = new OStatusTester($a, $b);