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