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