X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=tests%2Fatompub%2Fatompub_test.php;h=e23e4a711bb65b89666442edcd111548e9386401;hb=3ddfa4de931f4eb3083ac877898b5ee8b03a82f1;hp=047e2cebbec6bec4603bd15414c9eb24b3642205;hpb=654d1749da1f57f991dc03cab6a6787754b2c4eb;p=quix0rs-gnu-social.git diff --git a/tests/atompub/atompub_test.php b/tests/atompub/atompub_test.php index 047e2cebbe..e23e4a711b 100644 --- a/tests/atompub/atompub_test.php +++ b/tests/atompub/atompub_test.php @@ -1,7 +1,49 @@ +#!/usr/bin/env php . + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..')); + +$shortoptions = 'n:p:'; +$longoptions = array('nickname=', 'password=', 'dry-run'); + +$helptext = << --nickname= Nickname of account to post as + -p --password= Password for account + --dry-run Skip tests that modify the site (post, delete) + +END_OF_HELP; + +require_once INSTALLDIR.'/scripts/commandline.inc'; class AtomPubClient { + public $url; + private $user, $pass; + /** * * @param string $url collection feed URL @@ -16,76 +58,310 @@ class AtomPubClient } /** - * @param string $baseUrl to attempt feed discovery from - * @return AtomPubClient + * Set up an HTTPClient with auth for our resource. + * + * @param string $method + * @return HTTPClient */ - static function discoverFromPage($baseUrl) + private function httpClient($method='GET') { - + $client = new HTTPClient($this->url); + $client->setMethod($method); + $client->setAuth($this->user, $this->pass); + return $client; } function get() { - + $client = $this->httpClient('GET'); + $response = $client->send(); + if ($response->isOk()) { + return $response->getBody(); + } else { + throw new Exception("Bogus return code: " . $response->getStatus() . ': ' . $response->getBody()); + } } - - function post($stuff, $type='application/atom+xml;type=entry') + + /** + * Create a new resource by POSTing it to the collection. + * If successful, will return the URL representing the + * canonical location of the new resource. Neat! + * + * @param string $data + * @param string $type defaults to Atom entry + * @return string URL to the created resource + * + * @throws exceptions on failure + */ + function post($data, $type='application/atom+xml;type=entry') { - // post it up! - // optional 'Slug' header too - // .. receive .. - if ($response->getStatus() == '201') { - // yay - // MUST have a "Location" header - // if it has a Content-Location header, it MUST match Location - // and if it does, check the response body -- it should match what we posted, more or less. - } else { - throw new Exception("Expected HTTP 201 on POST, got " . $response->getStatus()); + $client = $this->httpClient('POST'); + $client->setHeader('Content-Type', $type); + // optional Slug header not used in this case + $client->setBody($data); + $response = $client->send(); + + if ($response->getStatus() != '201') { + throw new Exception("Expected HTTP 201 on POST, got " . $response->getStatus() . ': ' . $response->getBody()); } + $loc = $response->getHeader('Location'); + $contentLoc = $response->getHeader('Content-Location'); + + if (empty($loc)) { + throw new Exception("AtomPub POST response missing Location header."); + } + if (!empty($contentLoc)) { + if ($loc != $contentLoc) { + throw new Exception("AtomPub POST response Location and Content-Location headers do not match."); + } + + // If Content-Location and Location match, that means the response + // body is safe to interpret as the resource itself. + if ($type == 'application/atom+xml;type=entry') { + self::validateAtomEntry($response->getBody()); + } + } + + return $loc; } + /** + * Note that StatusNet currently doesn't allow PUT editing on notices. + * + * @param string $data + * @param string $type defaults to Atom entry + * @return true on success + * + * @throws exceptions on failure + */ function put($data, $type='application/atom+xml;type=entry') { - // PUT it up! - // must get a 200 back. - // unlike post, we don't get the location too. + $client = $this->httpClient('PUT'); + $client->setHeader('Content-Type', $type); + $client->setBody($data); + $response = $client->send(); + + if ($response->getStatus() != '200' && $response->getStatus() != '204') { + throw new Exception("Expected HTTP 200 or 204 on PUT, got " . $response->getStatus() . ': ' . $response->getBody()); + } + + return true; + } + + /** + * Delete the resource. + * + * @return true on success + * + * @throws exceptions on failure + */ + function delete() + { + $client = $this->httpClient('DELETE'); + $client->setBody($data); + $response = $client->send(); + + if ($response->getStatus() != '200' && $response->getStatus() != '204') { + throw new Exception("Expected HTTP 200 or 204 on DELETE, got " . $response->getStatus() . ': ' . $response->getBody()); + } + + return true; + } + + /** + * Ensure that the given string is a parseable Atom entry. + * + * @param string $str + * @return boolean + * @throws Exception on invalid input + */ + static function validateAtomEntry($str) + { + if (empty($str)) { + throw new Exception('Bad Atom entry: empty'); + } + $dom = new DOMDocument; + if (!$dom->loadXML($str)) { + throw new Exception('Bad Atom entry: XML is not well formed.'); + } + + $activity = new Activity($dom->documentRoot); + return true; + } + + static function entryEditURL($str) { + $dom = new DOMDocument; + $dom->loadXML($str); + $path = new DOMXPath($dom); + $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); + + $links = $path->query('/atom:entry/atom:link[@rel="edit"]', $dom->documentRoot); + if ($links && $links->length) { + if ($links->length > 1) { + throw new Exception('Bad Atom entry; has multiple rel=edit links.'); + } + $link = $links->item(0); + $url = $link->getAttribute('href'); + return $url; + } else { + throw new Exception('Atom entry lists no rel=edit link.'); + } + } + + static function entryId($str) { + $dom = new DOMDocument; + $dom->loadXML($str); + $path = new DOMXPath($dom); + $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); + + $links = $path->query('/atom:entry/atom:id', $dom->documentRoot); + if ($links && $links->length) { + if ($links->length > 1) { + throw new Exception('Bad Atom entry; has multiple id entries.'); + } + $link = $links->item(0); + $url = $link->textContent; + return $url; + } else { + throw new Exception('Atom entry lists no id.'); + } + } + + static function getEntryInFeed($str, $id) + { + $dom = new DOMDocument; + $dom->loadXML($str); + $path = new DOMXPath($dom); + $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); + + $query = '/atom:feed/atom:entry[atom:id="'.$id.'"]'; + $items = $path->query($query, $dom->documentRoot); + if ($items && $items->length) { + return $items->item(0); + } else { + return null; + } } } + +$user = get_option_value('n', 'nickname'); +$pass = get_option_value('p', 'password'); + +if (!$user) { + die("Must set a user: --nickname=\n"); +} +if (!$pass) { + die("Must set a password: --password=\n"); +} + // discover the feed... +// @fixme will this actually work? +$url = common_local_url('ApiTimelineUser', array('format' => 'atom', 'id' => $user)); + +echo "Collection URL is: $url\n"; + +$collection = new AtomPubClient($url, $user, $pass); // confirm the feed has edit links ..... ? -$pub = new AtomPubClient($url, $user, $pass); +echo "Posting an empty message (should fail)... "; +try { + $noticeUrl = $collection->post(''); + die("FAILED, succeeded!\n"); +} catch (Exception $e) { + echo "ok\n"; +} + +echo "Posting an invalid XML message (should fail)... "; +try { + $noticeUrl = $collection->post('barf'); + die("FAILED, succeeded!\n"); +} catch (Exception $e) { + echo "ok\n"; +} + +echo "Posting a valid XML but non-Atom message (should fail)... "; +try { + $noticeUrl = $collection->post('arfbarf'); + die("FAILED, succeeded!\n"); +} catch (Exception $e) { + echo "ok\n"; +} // post! -$target = $pub->post($atom); +$rand = mt_rand(0, 99999); +$atom = << + This is an AtomPub test post title ($rand) + This is an AtomPub test post content ($rand) + +END_ATOM; -// make sure it's accessible -// fetch $target -- should give us the atom entry -// edit URL should match +echo "Posting a new message... "; +$noticeUrl = $collection->post($atom); +echo "ok, got $noticeUrl\n"; -// refetch the feed; make sure the new entry is in there -// edit URL should match +echo "Fetching the new notice... "; +$notice = new AtomPubClient($noticeUrl, $user, $pass); +$body = $notice->get(); +AtomPubClient::validateAtomEntry($body); +echo "ok\n"; -// try editing! it should fail. -try { - $target2 = $pub->put($target, $atom2); - // FAIL! this shouldn't work. -} catch (AtomPubPermissionDeniedException $e) { - // yay +echo "Getting the notice ID URI... "; +$noticeUri = AtomPubClient::entryId($body); +echo "ok: $noticeUri\n"; + +echo "Confirming new entry points to itself right... "; +$editUrl = AtomPubClient::entryEditURL($body); +if ($editUrl != $noticeUrl) { + die("Entry lists edit URL as $editUrl, no match!\n"); } +echo "OK\n"; -// try deleting! -$pub->delete(); +echo "Refetching the collection... "; +$feed = $collection->get(); +echo "ok\n"; -// fetch $target -- it should be gone now +echo "Confirming new entry is in the feed... "; +$entry = AtomPubClient::getEntryInFeed($feed, $noticeUri); +if (!$entry) { + die("missing!\n"); +} +// edit URL should match +echo "ok\n"; -// fetch the feed again; the new entry should be gone again +echo "Editing notice (should fail)... "; +try { + $notice->put($target, $atom2); + die("ERROR: editing a notice should have failed.\n"); +} catch (Exception $e) { + echo "ok (failed as expected)\n"; +} +echo "Deleting notice... "; +$notice->delete(); +echo "ok\n"; +echo "Refetching deleted notice to confirm it's gone... "; +try { + $body = $notice->get(); + var_dump($body); + die("ERROR: notice should be gone now.\n"); +} catch (Exception $e) { + echo "ok\n"; +} +echo "Refetching the collection.. "; +$feed = $collection->get(); +echo "ok\n"; +echo "Confirming deleted notice is no longer in the feed... "; +$entry = AtomPubClient::getEntryInFeed($feed, $noticeUri); +if ($entry) { + die("still there!\n"); +} +echo "ok\n"; // make subscriptions // make some posts