]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/tests/remote-tests.php
b064114911212580df7cece401e451c70e7d99b2
[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('INSTALLDIR', dirname(dirname(dirname(dirname(__FILE__)))));
8 set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path());
9
10 require_once 'PEAR.php';
11 require_once 'Net/URL2.php';
12 require_once 'HTTP/Request2.php';
13
14
15 // ostatus test script, client-side :)
16
17 class TestBase
18 {
19     function log($str)
20     {
21         $args = func_get_args();
22         array_shift($args);
23
24         $msg = vsprintf($str, $args);
25         print $msg . "\n";
26     }
27
28     function assertEqual($a, $b)
29     {
30         if ($a != $b) {
31             throw new Exception("Failed to assert equality: expected $a, got $b");
32         }
33         return true;
34     }
35
36     function assertNotEqual($a, $b)
37     {
38         if ($a == $b) {
39             throw new Exception("Failed to assert inequality: expected not $a, got $b");
40         }
41         return true;
42     }
43
44     function assertTrue($a)
45     {
46         if (!$a) {
47             throw new Exception("Failed to assert true: got false");
48         }
49     }
50
51     function assertFalse($a)
52     {
53         if ($a) {
54             throw new Exception("Failed to assert false: got true");
55         }
56     }
57 }
58
59 class OStatusTester extends TestBase
60 {
61     /**
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)
64      */
65     function __construct($a, $b) {
66         $this->a = $a;
67         $this->b = $b;
68
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));
72     }
73
74     function run()
75     {
76         $this->setup();
77
78         $this->testLocalPost();
79         $this->testMentionUrl();
80         $this->testSubscribe();
81         $this->testUnsubscribe();
82
83         $this->log("DONE!");
84     }
85
86     function setup()
87     {
88         $this->pub->register();
89         $this->pub->assertRegistered();
90
91         $this->sub->register();
92         $this->sub->assertRegistered();
93     }
94
95     function testLocalPost()
96     {
97         $post = $this->pub->post("Local post, no subscribers yet.");
98         $this->assertNotEqual('', $post);
99
100         $post = $this->sub->post("Local post, no subscriptions yet.");
101         $this->assertNotEqual('', $post);
102     }
103
104     /**
105      * pub posts: @b/sub
106      */
107     function testMentionUrl()
108     {
109         $bits = parse_url($this->b);
110         $base = $bits['host'];
111         if (isset($bits['path'])) {
112             $base .= $bits['path'];
113         }
114         $name = $this->sub->username;
115
116         $post = $this->pub->post("@$base/$name should have this in home and replies");
117         $this->sub->assertReceived($post);
118     }
119
120     function testSubscribe()
121     {
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()));
127     }
128
129     function testUnsubscribe()
130     {
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()));
136     }
137
138 }
139
140 class SNTestClient extends TestBase
141 {
142     function __construct($base, $username, $password)
143     {
144         $this->basepath = $base;
145         $this->username = $username;
146         $this->password = $password;
147
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';
152     }
153
154     /**
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
159      * @return string
160      * @throws Exception on low-level error conditions
161      */
162     protected function hit($path, $params=array(), $auth=false, $cookies=array())
163     {
164         $url = $this->basepath . '/' . $path;
165
166         $http = new HTTP_Request2($url, 'POST');
167         if ($auth) {
168             $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC);
169         }
170         foreach ($cookies as $name => $val) {
171             $http->addCookie($name, $val);
172         }
173         $http->addPostParameter($params);
174         $response = $http->send();
175
176         $code = $response->getStatus();
177         if ($code < '200' || $code >= '400') {
178             throw new Exception("Failed API hit to $url: $code\n" . $response->getBody());
179         }
180
181         return $response;
182     }
183
184     /**
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
189      */
190     protected function web($path, $form, $params=array())
191     {
192         $url = $this->basepath . '/' . $path;
193         $http = new HTTP_Request2($url, 'GET');
194         $response = $http->send();
195
196         $dom = $this->checkWeb($url, 'GET', $response);
197         $cookies = array();
198         foreach ($response->getCookies() as $cookie) {
199             // @fixme check for expirations etc
200             $cookies[$cookie['name']] = $cookie['value'];
201         }
202
203         $form = $dom->getElementById($form);
204         if (!$form) {
205             throw new Exception("Form $form not found on $url");
206         }
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;
215                 }
216             }
217         }
218
219         $response = $this->hit($path, $params, false, $cookies);
220         $dom = $this->checkWeb($url, 'POST', $response);
221
222         return $dom;
223     }
224
225     protected function checkWeb($url, $method, $response)
226     {
227         $dom = new DOMDocument();
228         if (!$dom->loadHTML($response->getBody())) {
229             throw new Exception("Invalid HTML from $method to $url");
230         }
231
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);
237         }
238
239         return $dom;
240     }
241
242     protected function parseXml($path, $body)
243     {
244         $dom = new DOMDocument();
245         if ($dom->loadXML($body)) {
246             return $dom;
247         } else {
248             throw new Exception("Bogus XML data from $path:\n$body");
249         }
250     }
251
252     /**
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
258      */
259     protected function xml($path, $params=array())
260     {
261         $response = $this->hit($path, $params, true);
262         $body = $response->getBody();
263         return $this->parseXml($path, $body);
264     }
265
266     protected function parseJson($path, $body)
267     {
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']);
272             }
273             return $data;
274         } else {
275             throw new Exception("Bogus JSON data from $path:\n$body");
276         }
277     }
278
279     /**
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
286      */
287     protected function api($path, $style, $params=array())
288     {
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);
295         } else {
296             throw new Exception("API needs to be JSON, XML, or Atom");
297         }
298     }
299
300     /**
301      * Register the account.
302      *
303      * Unfortunately there's not an API method for registering, so we fake it.
304      */
305     function register()
306     {
307         $this->log("Registering user %s on %s",
308                    $this->username,
309                    $this->basepath);
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,
316                   'bio' => $this->bio,
317                   'license' => 1,
318                   'submit' => 'Register'));
319     }
320
321     /**
322      * @return string canonical URI/URL to profile page
323      */
324     function getProfileUri()
325     {
326         $data = $this->api('account/verify_credentials', 'json');
327         $id = $data['id'];
328         return $this->basepath . '/user/' . $id;
329     }
330
331     /**
332      * @return string human-friendly URL to profile page
333      */
334     function getProfileLink()
335     {
336         return $this->basepath . '/' . $this->username;
337     }
338
339     /**
340      * Check that the account has been registered and can be used.
341      * On failure, throws a test failure exception.
342      */
343     function assertRegistered()
344     {
345         $this->log("Confirming %s is registered on %s",
346                    $this->username,
347                    $this->basepath);
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']);
353     }
354
355     /**
356      * Post a given message from this account
357      * @param string $message
358      * @return string URL/URI of notice
359      * @todo reply, location options
360      */
361     function post($message)
362     {
363         $this->log("Posting notice as %s on %s: %s",
364                    $this->username,
365                    $this->basepath,
366                    $message);
367         $data = $this->api('statuses/update', 'json',
368             array('status' => $message));
369
370         $url = $this->basepath . '/notice/' . $data['id'];
371         return $url;
372     }
373
374     /**
375      * Check that this account has received the notice.
376      * @param string $notice_uri URI for the notice to check for
377      */
378     function assertReceived($notice_uri)
379     {
380         $timeout = 5;
381         $tries = 6;
382         while ($tries) {
383             $ok = $this->checkReceived($notice_uri);
384             if ($ok) {
385                 return true;
386             }
387             $tries--;
388             if ($tries) {
389                 $this->log("Didn't see it yet, waiting $timeout seconds");
390                 sleep($timeout);
391             }
392         }
393         throw new Exception("Message $notice_uri not received by $this->username");
394     }
395
396     /**
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.
400      *
401      * @param string $notice_uri
402      */
403     function checkReceived($notice_uri)
404     {
405         $this->log("Checking if %s on %s received notice %s",
406                    $this->username,
407                    $this->basepath,
408                    $notice_uri);
409         $params = array();
410         $dom = $this->api('statuses/home_timeline', 'atom', $params);
411
412         $xml = simplexml_import_dom($dom);
413         if (!$xml->entry) {
414             return false;
415         }
416         if (is_array($xml->entry)) {
417             $entries = $xml->entry;
418         } else {
419             $entries = array($xml->entry);
420         }
421         foreach ($entries as $entry) {
422             if ($entry->id == $notice_uri) {
423                 $this->log("found it $notice_uri");
424                 return true;
425             }
426             //$this->log("nope... " . $entry->id);
427         }
428         return false;
429     }
430
431     /**
432      * @param string $profile user page link or webfinger
433      */
434     function subscribe($profile)
435     {
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);
440     }
441
442     /**
443      * @param string $profile user page link or webfinger
444      */
445     function unsubscribe($profile)
446     {
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);
451     }
452
453     /**
454      * Check that this account is subscribed to the given profile.
455      * @param string $profile_uri URI for the profile to check for
456      * @return boolean
457      */
458     function hasSubscription($profile_uri)
459     {
460         $this->log("Checking if $this->username has a subscription to $profile_uri");
461
462         $me = $this->getProfileUri();
463         return $this->checkSubscription($me, $profile_uri);
464     }
465
466     /**
467      * Check that this account is subscribed to by the given profile.
468      * @param string $profile_uri URI for the profile to check for
469      * @return boolean
470      */
471     function hasSubscriber($profile_uri)
472     {
473         $this->log("Checking if $this->username is subscribed to by $profile_uri");
474
475         $me = $this->getProfileUri();
476         return $this->checkSubscription($profile_uri, $me);
477     }
478     
479     protected function checkSubscription($subscriber, $subscribed)
480     {
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#';
486
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");
497                         return true;
498                     }
499                 }
500                 $this->log("We found $subscriber but they don't follow $subscribed");
501                 return false;
502             }
503         }
504         $this->log("Can't find $subscriber in {$this->username}'s social graph.");
505         return false;
506     }
507
508 }
509
510 $args = array_slice($_SERVER['argv'], 1);
511 if (count($args) < 2) {
512     print <<<END_HELP
513 remote-tests.php <url1> <url2>
514   url1: base URL of a StatusNet instance
515   url2: base URL of another StatusNet instance
516
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.
520
521 END_HELP;
522 exit(1);
523 }
524
525 $a = $args[0];
526 $b = $args[1];
527
528 $tester = new OStatusTester($a, $b);
529 $tester->run();
530