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