]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/tests/remote-tests.php
Merge remote-tracking branch 'upstream/master' into social-master
[quix0rs-gnu-social.git] / plugins / OStatus / tests / remote-tests.php
1 <?php
2
3 if (php_sapi_name() != 'cli') {
4     die('not for web');
5 }
6
7 define('HTTP_TIMEOUT', 60); // ssslllloowwwww salmon if queues are off
8
9 define('INSTALLDIR', dirname(dirname(dirname(dirname(__FILE__)))));
10 set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path());
11
12 require_once 'PEAR.php';
13 require_once 'Net/URL2.php';
14 require_once 'HTTP/Request2.php';
15
16
17 // ostatus test script, client-side :)
18
19 class TestBase
20 {
21     function log($str)
22     {
23         $args = func_get_args();
24         array_shift($args);
25
26         $msg = vsprintf($str, $args);
27         print $msg . "\n";
28     }
29
30     function assertEqual($a, $b)
31     {
32         if ($a != $b) {
33             throw new Exception("Failed to assert equality: expected $a, got $b");
34         }
35         return true;
36     }
37
38     function assertNotEqual($a, $b)
39     {
40         if ($a == $b) {
41             throw new Exception("Failed to assert inequality: expected not $a, got $b");
42         }
43         return true;
44     }
45
46     function assertTrue($a)
47     {
48         if (!$a) {
49             throw new Exception("Failed to assert true: got false");
50         }
51     }
52
53     function assertFalse($a)
54     {
55         if ($a) {
56             throw new Exception("Failed to assert false: got true");
57         }
58     }
59 }
60
61 class OStatusTester extends TestBase
62 {
63     /**
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)
67      */
68     function __construct($a, $b, $timeout=60) {
69         $this->a = $a;
70         $this->b = $b;
71
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);
75
76         $this->group = 'group' . $base;
77     }
78
79     function run()
80     {
81         $this->setup();
82
83         $methods = get_class_methods($this);
84         foreach ($methods as $method) {
85             if (strtolower(substr($method, 0, 4)) == 'test') {
86                 print "\n";
87                 print "== $method ==\n";
88                 call_user_func(array($this, $method));
89             }
90         }
91
92         print "\n";
93         $this->log("DONE!");
94     }
95
96     function setup()
97     {
98         $this->pub->register();
99         $this->pub->assertRegistered();
100
101         $this->sub->register();
102         $this->sub->assertRegistered();
103     }
104
105     function testLocalPost()
106     {
107         $post = $this->pub->post("Local post, no subscribers yet.");
108         $this->assertNotEqual('', $post);
109
110         $post = $this->sub->post("Local post, no subscriptions yet.");
111         $this->assertNotEqual('', $post);
112     }
113
114     /**
115      * pub posts: @b/sub
116      */
117     function testMentionUrl()
118     {
119         $bits = parse_url($this->b);
120         $base = $bits['host'];
121         if (isset($bits['path'])) {
122             $base .= $bits['path'];
123         }
124         $name = $this->sub->username;
125
126         $post = $this->pub->post("@$base/$name should have this in home and replies");
127         $this->sub->assertReceived($post);
128     }
129
130     function testSubscribe()
131     {
132         $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
133         $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
134         $this->sub->subscribe($this->pub->getProfileLink());
135         $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
136         $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
137     }
138
139     function testPush()
140     {
141         $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
142         $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
143
144         $name = $this->sub->username;
145         $post = $this->pub->post("Regular post, which $name should get via PuSH");
146         $this->sub->assertReceived($post);
147     }
148
149     function testMentionSubscribee()
150     {
151         $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
152         $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri()));
153
154         $name = $this->pub->username;
155         $post = $this->sub->post("Just a quick note back to my remote subscribee @$name");
156         $this->pub->assertReceived($post);
157     }
158
159     function testUnsubscribe()
160     {
161         $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
162         $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
163         $this->sub->unsubscribe($this->pub->getProfileLink());
164         $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
165         $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
166     }
167
168     function testCreateGroup()
169     {
170         $this->groupUrl = $this->pub->createGroup($this->group);
171         $this->assertTrue(!empty($this->groupUrl));
172     }
173
174     function testJoinGroup()
175     {
176         #$this->assertFalse($this->sub->inGroup($this->groupUrl));
177         $this->sub->joinGroup($this->groupUrl);
178         #$this->assertTrue($this->sub->inGroup($this->groupUrl));
179     }
180
181     function testLocalGroupPost()
182     {
183         $post = $this->pub->post("Group post from local to !{$this->group}, should go out over push.");
184         $this->assertNotEqual('', $post);
185         $this->sub->assertReceived($post);
186     }
187
188     function testRemoteGroupPost()
189     {
190         $post = $this->sub->post("Group post from remote to !{$this->group}, should come in over salmon.");
191         $this->assertNotEqual('', $post);
192         $this->pub->assertReceived($post);
193     }
194
195     function testLeaveGroup()
196     {
197         #$this->assertTrue($this->sub->inGroup($this->groupUrl));
198         $this->sub->leaveGroup($this->groupUrl);
199         #$this->assertFalse($this->sub->inGroup($this->groupUrl));
200     }
201 }
202
203 class SNTestClient extends TestBase
204 {
205     function __construct($base, $username, $password, $timeout=60)
206     {
207         $this->basepath = $base;
208         $this->username = $username;
209         $this->password = $password;
210         $this->timeout = $timeout;
211
212         $this->fullname = ucfirst($username) . ' Smith';
213         $this->homepage = 'http://example.org/' . $username;
214         $this->bio = 'Stub account for OStatus tests.';
215         $this->location = 'Montreal, QC';
216     }
217
218     /**
219      * Make a low-level web hit to this site, with authentication.
220      * @param string $path URL fragment for something under the base path
221      * @param array $params POST parameters to send
222      * @param boolean $auth whether to include auth data
223      * @return string
224      * @throws Exception on low-level error conditions
225      */
226     protected function hit($path, $params=array(), $auth=false, $cookies=array())
227     {
228         $url = $this->basepath . '/' . $path;
229
230         $http = new HTTP_Request2($url, 'POST', array('timeout' => $this->timeout));
231         if ($auth) {
232             $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC);
233         }
234         foreach ($cookies as $name => $val) {
235             $http->addCookie($name, $val);
236         }
237         $http->addPostParameter($params);
238         $response = $http->send();
239
240         $code = $response->getStatus();
241         if ($code < '200' || $code >= '400') {
242             throw new Exception("Failed API hit to $url: $code\n" . $response->getBody());
243         }
244
245         return $response;
246     }
247
248     /**
249      * Make a hit to a web form, without authentication but with a session.
250      * @param string $path URL fragment relative to site base
251      * @param string $form id of web form to pull initial parameters from
252      * @param array $params POST parameters, will be merged with defaults in form
253      */
254     protected function web($path, $form, $params=array())
255     {
256         $url = $this->basepath . '/' . $path;
257         $http = new HTTP_Request2($url, 'GET', array('timeout' => $this->timeout));
258         $response = $http->send();
259
260         $dom = $this->checkWeb($url, 'GET', $response);
261         $cookies = array();
262         foreach ($response->getCookies() as $cookie) {
263             // @fixme check for expirations etc
264             $cookies[$cookie['name']] = $cookie['value'];
265         }
266
267         $form = $dom->getElementById($form);
268         if (!$form) {
269             throw new Exception("Form $form not found on $url");
270         }
271         $inputs = $form->getElementsByTagName('input');
272         foreach ($inputs as $item) {
273             $type = $item->getAttribute('type');
274             if ($type != 'check') {
275                 $name = $item->getAttribute('name');
276                 $val = $item->getAttribute('value');
277                 if ($name && $val && !isset($params[$name])) {
278                     $params[$name] = $val;
279                 }
280             }
281         }
282
283         $response = $this->hit($path, $params, false, $cookies);
284         $dom = $this->checkWeb($url, 'POST', $response);
285
286         return $dom;
287     }
288
289     protected function checkWeb($url, $method, $response)
290     {
291         $dom = new DOMDocument();
292         if (!$dom->loadHTML($response->getBody())) {
293             throw new Exception("Invalid HTML from $method to $url");
294         }
295
296         $xpath = new DOMXPath($dom);
297         $error = $xpath->query('//p[@class="error"]');
298         if ($error && $error->length) {
299             throw new Exception("Error on $method to $url: " .
300                                 $error->item(0)->textContent);
301         }
302
303         return $dom;
304     }
305
306     protected function parseXml($path, $body)
307     {
308         $dom = new DOMDocument();
309         if ($dom->loadXML($body)) {
310             return $dom;
311         } else {
312             throw new Exception("Bogus XML data from $path:\n$body");
313         }
314     }
315
316     /**
317      * Make a hit to a REST-y XML page on the site, without authentication.
318      * @param string $path URL fragment for something relative to base
319      * @param array $params POST parameters to send
320      * @return DOMDocument
321      * @throws Exception on low-level error conditions
322      */
323     protected function xml($path, $params=array())
324     {
325         $response = $this->hit($path, $params, true);
326         $body = $response->getBody();
327         return $this->parseXml($path, $body);
328     }
329
330     protected function parseJson($path, $body)
331     {
332         $data = json_decode($body, true);
333         if ($data !== null) {
334             if (!empty($data['error'])) {
335                 throw new Exception("JSON API returned error: " . $data['error']);
336             }
337             return $data;
338         } else {
339             throw new Exception("Bogus JSON data from $path:\n$body");
340         }
341     }
342
343     /**
344      * Make an API hit to this site, with authentication.
345      * @param string $path URL fragment for something under 'api' folder
346      * @param string $style one of 'json', 'xml', or 'atom'
347      * @param array $params POST parameters to send
348      * @return mixed associative array for JSON, DOMDocument for XML/Atom
349      * @throws Exception on low-level error conditions
350      */
351     protected function api($path, $style, $params=array())
352     {
353         $response = $this->hit("api/$path.$style", $params, true);
354         $body = $response->getBody();
355         if ($style == 'json') {
356             return $this->parseJson($path, $body);
357         } else if ($style == 'xml' || $style == 'atom') {
358             return $this->parseXml($path, $body);
359         } else {
360             throw new Exception("API needs to be JSON, XML, or Atom");
361         }
362     }
363
364     /**
365      * Register the account.
366      *
367      * Unfortunately there's not an API method for registering, so we fake it.
368      */
369     function register()
370     {
371         $this->log("Registering user %s on %s",
372                    $this->username,
373                    $this->basepath);
374         $ret = $this->web('main/register', 'form_register',
375             array('nickname' => $this->username,
376                   'password' => $this->password,
377                   'confirm' => $this->password,
378                   'fullname' => $this->fullname,
379                   'homepage' => $this->homepage,
380                   'bio' => $this->bio,
381                   'license' => 1,
382                   'submit' => 'Register'));
383     }
384
385     /**
386      * @return string canonical URI/URL to profile page
387      */
388     function getProfileUri()
389     {
390         $data = $this->api('account/verify_credentials', 'json');
391         $id = $data['id'];
392         return $this->basepath . '/user/' . $id;
393     }
394
395     /**
396      * @return string human-friendly URL to profile page
397      */
398     function getProfileLink()
399     {
400         return $this->basepath . '/' . $this->username;
401     }
402
403     /**
404      * Check that the account has been registered and can be used.
405      * On failure, throws a test failure exception.
406      */
407     function assertRegistered()
408     {
409         $this->log("Confirming %s is registered on %s",
410                    $this->username,
411                    $this->basepath);
412         $data = $this->api('account/verify_credentials', 'json');
413         $this->assertEqual($this->username, $data['screen_name']);
414         $this->assertEqual($this->fullname, $data['name']);
415         $this->assertEqual($this->homepage, $data['url']);
416         $this->assertEqual($this->bio, $data['description']);
417         $this->log("  looks good!");
418     }
419
420     /**
421      * Post a given message from this account
422      * @param string $message
423      * @return string URL/URI of notice
424      * @todo reply, location options
425      */
426     function post($message)
427     {
428         $this->log("Posting notice as %s on %s: %s",
429                    $this->username,
430                    $this->basepath,
431                    $message);
432         $data = $this->api('statuses/update', 'json',
433             array('status' => $message));
434
435         $url = $this->basepath . '/notice/' . $data['id'];
436         return $url;
437     }
438
439     /**
440      * Check that this account has received the notice.
441      * @param string $notice_uri URI for the notice to check for
442      */
443     function assertReceived($notice_uri)
444     {
445         $timeout = 5;
446         $tries = 6;
447         while ($tries) {
448             $ok = $this->checkReceived($notice_uri);
449             if ($ok) {
450                 return true;
451             }
452             $tries--;
453             if ($tries) {
454                 $this->log("  didn't see it yet, waiting $timeout seconds");
455                 sleep($timeout);
456             }
457         }
458         throw new Exception("  message $notice_uri not received by $this->username");
459     }
460
461     /**
462      * Pull the user's home timeline to check if a notice with the given
463      * source URL has been received recently.
464      * If we don't see it, we'll try a couple more times up to 10 seconds.
465      *
466      * @param string $notice_uri
467      */
468     function checkReceived($notice_uri)
469     {
470         $this->log("Checking if %s on %s received notice %s",
471                    $this->username,
472                    $this->basepath,
473                    $notice_uri);
474         $params = array();
475         $dom = $this->api('statuses/home_timeline', 'atom', $params);
476
477         $xml = simplexml_import_dom($dom);
478         if (!$xml->entry) {
479             return false;
480         }
481         if (is_array($xml->entry)) {
482             $entries = $xml->entry;
483         } else {
484             $entries = array($xml->entry);
485         }
486         foreach ($entries as $entry) {
487             if ($entry->id == $notice_uri) {
488                 $this->log("  found it $notice_uri");
489                 return true;
490             }
491         }
492         return false;
493     }
494
495     /**
496      * @param string $profile user page link or webfinger
497      */
498     function subscribe($profile)
499     {
500         // This uses the command interface, since there's not currently
501         // a friendly Twit-API way to do a fresh remote subscription and
502         // the web form's a pain to use.
503         $this->post('follow ' . $profile);
504     }
505
506     /**
507      * @param string $profile user page link or webfinger
508      */
509     function unsubscribe($profile)
510     {
511         // This uses the command interface, since there's not currently
512         // a friendly Twit-API way to do a fresh remote subscription and
513         // the web form's a pain to use.
514         $this->post('leave ' . $profile);
515     }
516
517     /**
518      * Check that this account is subscribed to the given profile.
519      * @param string $profile_uri URI for the profile to check for
520      * @return boolean
521      */
522     function hasSubscription($profile_uri)
523     {
524         $this->log("Checking if $this->username has a subscription to $profile_uri");
525
526         $me = $this->getProfileUri();
527         return $this->checkSubscription($me, $profile_uri);
528     }
529
530     /**
531      * Check that this account is subscribed to by the given profile.
532      * @param string $profile_uri URI for the profile to check for
533      * @return boolean
534      */
535     function hasSubscriber($profile_uri)
536     {
537         $this->log("Checking if $this->username is subscribed to by $profile_uri");
538
539         $me = $this->getProfileUri();
540         return $this->checkSubscription($profile_uri, $me);
541     }
542
543     protected function checkSubscription($subscriber, $subscribed)
544     {
545         // Using FOAF as the API methods for checking the social graph
546         // currently are unfriendly to remote profiles
547         $ns_foaf = 'http://xmlns.com/foaf/0.1/';
548         $ns_sioc = 'http://rdfs.org/sioc/ns#';
549         $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
550
551         $dom = $this->xml($this->username . '/foaf');
552         $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent');
553         foreach ($agents as $agent) {
554             $agent_uri = $agent->getAttributeNS($ns_rdf, 'about');
555             if ($agent_uri == $subscriber) {
556                 $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows');
557                 foreach ($follows as $follow) {
558                     $target = $follow->getAttributeNS($ns_rdf, 'resource');
559                     if ($target == ($subscribed . '#acct')) {
560                         $this->log("  confirmed $subscriber subscribed to $subscribed");
561                         return true;
562                     }
563                 }
564                 $this->log("  we found $subscriber but they don't follow $subscribed");
565                 return false;
566             }
567         }
568         $this->log("  can't find $subscriber in {$this->username}'s social graph.");
569         return false;
570     }
571
572     /**
573      * Create a group on this site.
574      *
575      * @param string $nickname
576      * @param array $options
577      * @return string: profile URL for the group
578      */
579     function createGroup($nickname, $options=array()) {
580         $this->log("Creating group as %s on %s: %s",
581                    $this->username,
582                    $this->basepath,
583                    $nickname);
584
585         $data = $this->api('statusnet/groups/create', 'json',
586             array_merge(array('nickname' => $nickname), $options));
587         $url = $data['url'];
588
589         if ($url) {
590             $this->log('  created as %s', $url);
591         } else {
592             $this->log('  failed? %s', var_export($data, true));
593         }
594         return $url;
595     }
596
597     function groupInfo($nickname) {
598         $data = $this->api('statusnet/groups/show', 'json', array(
599             'id' => $nickname
600         ));
601     }
602
603     /**
604      * Join a group.
605      *
606      * @param string $group nickname or URL
607      */
608     function joinGroup($group) {
609         $this->post('join ' . $group);
610     }
611
612     /**
613      * Leave a group.
614      *
615      * @param string $group nickname or URL
616      */
617     function leaveGroup($group) {
618         $this->post('drop ' . $group);
619     }
620
621     /**
622      *
623      * @param string $nickname
624      * @return
625      */
626     function inGroup($nickname) {
627         // @todo
628     }
629 }
630
631 // @fixme switch to commandline.inc?
632 $timeout = HTTP_TIMEOUT;
633
634 $args = array();
635 $options = array();
636 foreach (array_slice($_SERVER['argv'], 1) as $arg) {
637     if (substr($arg, 0, 2) == '--') {
638         $bits = explode('=', substr($arg, 2), 2);
639         if (count($bits == 2)) {
640             list($key, $val) = $bits;
641             $options[$key] = $val;
642         } else {
643             list($key) = $bits;
644             $options[$key] = true;
645         }
646     } else {
647         $args[] = $arg;
648     }
649 }
650 if (count($args) < 2) {
651     print <<<END_HELP
652 remote-tests.php [options] <url1> <url2>
653   --timeout=## change HTTP timeout from default {$timeout}s
654   url1: base URL of a StatusNet instance
655   url2: base URL of another StatusNet instance
656
657 This will register user accounts on the two given StatusNet instances
658 and run some tests to confirm that OStatus subscription and posting
659 between the two sites works correctly.
660
661 END_HELP;
662 exit(1);
663 }
664
665 $a = $args[0];
666 $b = $args[1];
667 if (isset($options['timeout'])) {
668     $timeout = intval($options['timeout']);
669 }
670
671 $tester = new OStatusTester($a, $b, $timeout);
672 $tester->run();