]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - tests/atompub/atompub_test.php
Merge branch '0.9.x' into 1.0.x
[quix0rs-gnu-social.git] / tests / atompub / atompub_test.php
1 #!/usr/bin/env php
2 <?php
3 /*
4  * StatusNet - the distributed open-source microblogging tool
5  * Copyright (C) 2010, StatusNet, Inc.
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20
21 define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..'));
22
23 $shortoptions = 'n:p:';
24 $longoptions = array('nickname=', 'password=', 'dry-run');
25
26 $helptext = <<<END_OF_HELP
27 USAGE: atompub_test.php [options]
28
29 Runs some tests on the AtomPub interface for the site. You must provide
30 a user account to authenticate as; it will be used to make some test
31 posts on the site.
32
33 Options:
34   -n<user>  --nickname=<user>  Nickname of account to post as
35   -p<pass>  --password=<pass>  Password for account
36   --dry-run                    Skip tests that modify the site (post, delete)
37
38 END_OF_HELP;
39
40 require_once INSTALLDIR.'/scripts/commandline.inc';
41
42 class AtomPubClient
43 {
44     public $url;
45     private $user, $pass;
46
47     /**
48      *
49      * @param string $url collection feed URL
50      * @param string $user auth username
51      * @param string $pass auth password
52      */
53     function __construct($url, $user, $pass)
54     {
55         $this->url = $url;
56         $this->user = $user;
57         $this->pass = $pass;
58     }
59
60     /**
61      * Set up an HTTPClient with auth for our resource.
62      *
63      * @param string $method
64      * @return HTTPClient
65      */
66     private function httpClient($method='GET')
67     {
68         $client = new HTTPClient($this->url);
69         $client->setMethod($method);
70         $client->setAuth($this->user, $this->pass);
71         return $client;
72     }
73
74     function get()
75     {
76         $client = $this->httpClient('GET');
77         $response = $client->send();
78         if ($response->isOk()) {
79             return $response->getBody();
80         } else {
81             throw new Exception("Bogus return code: " . $response->getStatus() . ': ' . $response->getBody());
82         }
83     }
84
85     /**
86      * Create a new resource by POSTing it to the collection.
87      * If successful, will return the URL representing the
88      * canonical location of the new resource. Neat!
89      *
90      * @param string $data
91      * @param string $type defaults to Atom entry
92      * @return string URL to the created resource
93      *
94      * @throws exceptions on failure
95      */
96     function post($data, $type='application/atom+xml;type=entry')
97     {
98         $client = $this->httpClient('POST');
99         $client->setHeader('Content-Type', $type);
100         // optional Slug header not used in this case
101         $client->setBody($data);
102         $response = $client->send();
103
104         if ($response->getStatus() != '201') {
105             throw new Exception("Expected HTTP 201 on POST, got " . $response->getStatus() . ': ' . $response->getBody());
106         }
107         $loc = $response->getHeader('Location');
108         $contentLoc = $response->getHeader('Content-Location');
109
110         if (empty($loc)) {
111             throw new Exception("AtomPub POST response missing Location header.");
112         }
113         if (!empty($contentLoc)) {
114             if ($loc != $contentLoc) {
115                 throw new Exception("AtomPub POST response Location and Content-Location headers do not match.");
116             }
117
118             // If Content-Location and Location match, that means the response
119             // body is safe to interpret as the resource itself.
120             if ($type == 'application/atom+xml;type=entry') {
121                 self::validateAtomEntry($response->getBody());
122             }
123         }
124
125         return $loc;
126     }
127
128     /**
129      * Note that StatusNet currently doesn't allow PUT editing on notices.
130      *
131      * @param string $data
132      * @param string $type defaults to Atom entry
133      * @return true on success
134      *
135      * @throws exceptions on failure
136      */
137     function put($data, $type='application/atom+xml;type=entry')
138     {
139         $client = $this->httpClient('PUT');
140         $client->setHeader('Content-Type', $type);
141         $client->setBody($data);
142         $response = $client->send();
143
144         if ($response->getStatus() != '200' && $response->getStatus() != '204') {
145             throw new Exception("Expected HTTP 200 or 204 on PUT, got " . $response->getStatus() . ': ' . $response->getBody());
146         }
147
148         return true;
149     }
150
151     /**
152      * Delete the resource.
153      *
154      * @return true on success
155      *
156      * @throws exceptions on failure
157      */
158     function delete()
159     {
160         $client = $this->httpClient('DELETE');
161         $client->setBody($data);
162         $response = $client->send();
163
164         if ($response->getStatus() != '200' && $response->getStatus() != '204') {
165             throw new Exception("Expected HTTP 200 or 204 on DELETE, got " . $response->getStatus() . ': ' . $response->getBody());
166         }
167
168         return true;
169     }
170
171     /**
172      * Ensure that the given string is a parseable Atom entry.
173      *
174      * @param string $str
175      * @return boolean
176      * @throws Exception on invalid input
177      */
178     static function validateAtomEntry($str)
179     {
180         if (empty($str)) {
181             throw new Exception('Bad Atom entry: empty');
182         }
183         $dom = new DOMDocument;
184         if (!$dom->loadXML($str)) {
185             throw new Exception('Bad Atom entry: XML is not well formed.');
186         }
187
188         $activity = new Activity($dom->documentRoot);
189         return true;
190     }
191
192     static function entryEditURL($str) {
193         $dom = new DOMDocument;
194         $dom->loadXML($str);
195         $path = new DOMXPath($dom);
196         $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
197
198         $links = $path->query('/atom:entry/atom:link[@rel="edit"]', $dom->documentRoot);
199         if ($links && $links->length) {
200             if ($links->length > 1) {
201                 throw new Exception('Bad Atom entry; has multiple rel=edit links.');
202             }
203             $link = $links->item(0);
204             $url = $link->getAttribute('href');
205             return $url;
206         } else {
207             throw new Exception('Atom entry lists no rel=edit link.');
208         }
209     }
210
211     static function entryId($str) {
212         $dom = new DOMDocument;
213         $dom->loadXML($str);
214         $path = new DOMXPath($dom);
215         $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
216
217         $links = $path->query('/atom:entry/atom:id', $dom->documentRoot);
218         if ($links && $links->length) {
219             if ($links->length > 1) {
220                 throw new Exception('Bad Atom entry; has multiple id entries.');
221             }
222             $link = $links->item(0);
223             $url = $link->textContent;
224             return $url;
225         } else {
226             throw new Exception('Atom entry lists no id.');
227         }
228     }
229
230     static function getEntryInFeed($str, $id)
231     {
232         $dom = new DOMDocument;
233         $dom->loadXML($str);
234         $path = new DOMXPath($dom);
235         $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
236
237         $query = '/atom:feed/atom:entry[atom:id="'.$id.'"]';
238         $items = $path->query($query, $dom->documentRoot);
239         if ($items && $items->length) {
240             return $items->item(0);
241         } else {
242             return null;
243         }
244     }
245 }
246
247
248 $user = get_option_value('n', 'nickname');
249 $pass = get_option_value('p', 'password');
250
251 if (!$user) {
252     die("Must set a user: --nickname=<username>\n");
253 }
254 if (!$pass) {
255     die("Must set a password: --password=<username>\n");
256 }
257
258 // discover the feed...
259 // @fixme will this actually work?
260 $url = common_local_url('ApiTimelineUser', array('format' => 'atom', 'id' => $user));
261
262 echo "Collection URL is: $url\n";
263
264 $collection = new AtomPubClient($url, $user, $pass);
265
266 // confirm the feed has edit links ..... ?
267
268 echo "Posting an empty message (should fail)... ";
269 try {
270     $noticeUrl = $collection->post('');
271     die("FAILED, succeeded!\n");
272 } catch (Exception $e) {
273     echo "ok\n";
274 }
275
276 echo "Posting an invalid XML message (should fail)... ";
277 try {
278     $noticeUrl = $collection->post('<feed<entry>barf</yomomma>');
279     die("FAILED, succeeded!\n");
280 } catch (Exception $e) {
281     echo "ok\n";
282 }
283
284 echo "Posting a valid XML but non-Atom message (should fail)... ";
285 try {
286     $noticeUrl = $collection->post('<feed xmlns="http://notatom.com"><id>arf</id><entry><id>barf</id></entry></feed>');
287     die("FAILED, succeeded!\n");
288 } catch (Exception $e) {
289     echo "ok\n";
290 }
291
292 // post!
293 $rand = mt_rand(0, 99999);
294 $atom = <<<END_ATOM
295 <entry xmlns="http://www.w3.org/2005/Atom">
296     <title>This is an AtomPub test post title ($rand)</title>
297     <content>This is an AtomPub test post content ($rand)</content>
298 </entry>
299 END_ATOM;
300
301 echo "Posting a new message... ";
302 $noticeUrl = $collection->post($atom);
303 echo "ok, got $noticeUrl\n";
304
305 echo "Fetching the new notice... ";
306 $notice = new AtomPubClient($noticeUrl, $user, $pass);
307 $body = $notice->get();
308 AtomPubClient::validateAtomEntry($body);
309 echo "ok\n";
310
311 echo "Getting the notice ID URI... ";
312 $noticeUri = AtomPubClient::entryId($body);
313 echo "ok: $noticeUri\n";
314
315 echo "Confirming new entry points to itself right... ";
316 $editUrl = AtomPubClient::entryEditURL($body);
317 if ($editUrl != $noticeUrl) {
318     die("Entry lists edit URL as $editUrl, no match!\n");
319 }
320 echo "OK\n";
321
322 echo "Refetching the collection... ";
323 $feed = $collection->get();
324 echo "ok\n";
325
326 echo "Confirming new entry is in the feed... ";
327 $entry = AtomPubClient::getEntryInFeed($feed, $noticeUri);
328 if (!$entry) {
329     die("missing!\n");
330 }
331 //  edit URL should match
332 echo "ok\n";
333
334 echo "Editing notice (should fail)... ";
335 try {
336     $notice->put($target, $atom2);
337     die("ERROR: editing a notice should have failed.\n");
338 } catch (Exception $e) {
339     echo "ok (failed as expected)\n";
340 }
341
342 echo "Deleting notice... ";
343 $notice->delete();
344 echo "ok\n";
345
346 echo "Refetching deleted notice to confirm it's gone... ";
347 try {
348     $body = $notice->get();
349     var_dump($body);
350     die("ERROR: notice should be gone now.\n");
351 } catch (Exception $e) {
352     echo "ok\n";
353 }
354
355 echo "Refetching the collection.. ";
356 $feed = $collection->get();
357 echo "ok\n";
358
359 echo "Confirming deleted notice is no longer in the feed... ";
360 $entry = AtomPubClient::getEntryInFeed($feed, $noticeUri);
361 if ($entry) {
362     die("still there!\n");
363 }
364 echo "ok\n";
365
366 // make subscriptions
367 // make some posts
368 // make sure the posts go through or not depending on the subs
369 // remove subscriptions
370 // test that they don't go through now
371
372 // group memberships too
373
374
375
376
377 // make sure we can't post to someone else's feed!
378 // make sure we can't delete someone else's messages
379 // make sure we can't create/delete someone else's subscriptions
380 // make sure we can't create/delete someone else's group memberships
381