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