# t/atom-post.t # # Test post via ATOM protocol. # # This code was forked from the LiveJournal project owned and operated # by Live Journal, Inc. The code has been modified and expanded by # Dreamwidth Studios, LLC. These files were originally licensed under # the terms of the license supplied by Live Journal, Inc, which can # currently be found at: # # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt # # In accordance with the original license, this code and all its # modifications are provided under the GNU General Public License. # A copy of that license can be found in the LICENSE file included as # part of this distribution. use strict; use warnings; use Test::More; BEGIN { $LJ::_T_CONFIG = 1; require "$ENV{LJHOME}/cgi-bin/ljlib.pl"; } use LJ::Test; plan skip_all => "AtomAPI authentication is currently broken -- remove?"; if (LJ::Test::check_memcache) { plan tests => 103; } else { plan skip_all => "Memcache configured but not active."; } use XML::Atom::Client; use XML::Atom::Entry; use XML::Atom::Category; use XML::Atom::Feed; use DW::Routing; my $u = temp_user(); my $pass = "foopass"; $u->set_password($pass); $u->update_self( { status => 'A' } ); my $api = XML::Atom::Client->new( Version => 1 ); my $r; sub do_request { my ( $method, $uri, %opts ) = @_; my $authenticate = delete $opts{authenticate}; my $data = delete $opts{data} || {}; my $remote = delete $opts{remote} || $u; my $password = delete $opts{password} || $remote->password; $uri =~ m!https?://([^.]+)!; my $user_subdomain = $1 eq "www" ? "" : $1; my %routing_data = (); $routing_data{username} = $user_subdomain if $user_subdomain; my $input = delete $data->{input}; $r = routing_request( $uri, method => $method, content => $input, setup_http_request => sub { if ($authenticate) { $api->username( $remote->username ); $api->password($password); $api->munge_request( $_[0] ); } }, setup_dw_request => sub { $_[0]->pnote( $_ => $data->{$_} ) foreach %$data; }, routing_data => \%routing_data, ); } # check subject, etc # items that are not defined in the hash to be checked against are ignored sub check_entry { my ( $atom_entry, $entry_info, $journal ) = @_; local $Test::Builder::Level = $Test::Builder::Level + 1; subtest "check_entry $entry_info->{id} - $entry_info->{title}" => sub { ok( $atom_entry, "Got an atom entry back from the server" ); is( $atom_entry->title, $entry_info->{title}, "atom entry has right title" ) if defined $entry_info->{title}; # having the content body be of type HTML # causes newlines to appear for some reason when we try to extract the content as a string # so let's just work around it; it should be harmless (as_xml doesn't contain the extra newlines) my $event_raw = $entry_info->{content}; like( $atom_entry->content->body, qr/\s*$event_raw\s*/, "atom entry has right content" ) if defined $entry_info->{content}; is( $atom_entry->id, $entry_info->{atom_id}, "atom id" ) if defined $entry_info->{atom_id}; is( $atom_entry->author->name, $entry_info->{author}, "atom entry author" ) if defined $entry_info->{author}; if ( defined $entry_info->{url} ) { my @links = $atom_entry->link; is( scalar @links, 2, "got back two links" ); foreach my $link (@links) { if ( $link->rel eq "edit" ) { is( $link->href, $journal->atom_base . "/entries/$entry_info->{id}", "edit link" ); } else { # alternate is( $link->href, $entry_info->{url}, "entry link" ); } } } if ( defined $entry_info->{categories} ) { my %tags = map { $_ => 1 } @{ $entry_info->{categories} || [] }; my %categories = map { $_->term => 1 } $atom_entry->category; is( scalar keys %categories, 2, "got back multiple categories" ); is_deeply( {%categories}, {%tags}, "got back the categories we sent in" ); } }; } note("Authentication"); do_request( GET => $u->atom_service_document ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Did not pass any authorization information." ); is( $r->content_type, "text/plain", "Error content type" ); # intentionally break authorization do_request( GET => $u->atom_service_document, authenticate => 1, password => $u->password x 3 ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Passed wrong authorization information." ); do_request( GET => $u->atom_service_document, authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Successful authentication." ); note("Service document introspection."); do_request( POST => $u->atom_service_document ); is( $r->status, $r->HTTP_METHOD_NOT_ALLOWED, "Service document needs GET (even unauthorized)" ); do_request( POST => $u->atom_service_document, authenticate => 1 ); is( $r->status, $r->HTTP_METHOD_NOT_ALLOWED, "Service document needs GET." ); do_request( GET => $u->atom_service_document, authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Got service document." ); like( $r->content_type, qr#^\Qapplication/atomsvc+xml\E#, "Service content type" ); my $service_document_xml = $r->response_content; note("Categories document."); # populate journal with some tags my @journal_tags = qw( a b c ); LJ::Tags::create_usertag( $u, join( ", ", @journal_tags ), { display => 1 } ); do_request( GET => $u->atom_base . "/entries/tags" ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Categories document protected by authorization." ); do_request( POST => $u->atom_base . "/entries/tags", authenticate => 1 ); is( $r->status, $r->HTTP_METHOD_NOT_ALLOWED, "Categories document needs GET." ); do_request( GET => $u->atom_base . "/entries/tags", authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Got categories document." ); like( $r->content_type, qr#^\Qapplication/atomcat+xml\E#, "Categories document type" ); my $categories_document_xml = $r->response_content; SKIP: { skip "No XML::Atom::Service/XML::Atom::Categories module installed.", 10 unless eval "use XML::Atom::Service; use XML::Atom::Categories; 1;"; my $service = XML::Atom::Service->new( \$service_document_xml ); ok( $service, "Got service document." ); my @workspaces = $service->workspace; is( scalar @workspaces, 1, "One workspace" ); is( $workspaces[0]->title, $u->user, "Workspace title" ); my @collections = $workspaces[0]->collections; is( scalar @collections, 1, "One collection" ); is( $collections[0]->title, "Entries", "Entries collection title" ); is( $collections[0]->href, $u->atom_base . "/entries", "Entries collection uri" ); my @categories = $collections[0]->categories; is( scalar @categories, 1, "One categories link" ); is( $categories[0]->href, $u->atom_base . "/entries/tags", "Categories collection uri" ); my $categories = XML::Atom::Categories->new( \$categories_document_xml ); @categories = $categories->category; is( scalar @categories, 3, "three existing categories" ); is_deeply( { map { $_->term => 1 } @categories }, { map { $_ => 1 } @journal_tags }, "Journal tags match fetched categories" ); } my $atom_entry; # an XML::Atom::Entry object my $entry_obj; # an LJ::Entry object my $atom_entry_server; # an XML::Atom::Entry object retrieved from the server $atom_entry = XML::Atom::Entry->new( Version => 1 ); my $title = "New Post"; my $content = "Content of my post at " . rand(); my @tags = qw( foo bar ); $atom_entry->title($title); $atom_entry->content($content); foreach my $tag (@tags) { my $category = XML::Atom::Category->new( Version => 1 ); $category->term($tag); $atom_entry->add_category($category); } note("Create an entry."); do_request( POST => $u->atom_base . "/entries", data => { input => $atom_entry->as_xml } ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Entry creation protected by authorization." ); do_request( POST => $u->atom_base . "/entries", authenticate => 1, data => { input => $atom_entry->as_xml } ); is( $r->status, $r->HTTP_CREATED, "POSTed new entry" ); is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" ); note("Double-check posted entry."); $entry_obj = LJ::Entry->new( $u, jitemid => 1 ); ok( $entry_obj, "got entry" ); ok( $entry_obj->valid, "entry is valid" ); is( $entry_obj->subject_raw, $title, "item has right title" ); is( $entry_obj->event_raw, $content, "item has right content" ); $atom_entry_server = XML::Atom::Entry->new( \$r->response_content ); check_entry( $atom_entry_server, { id => $entry_obj->jitemid, title => $atom_entry_server->title, content => $atom_entry_server->content->body, url => $entry_obj->url, author => $u->name_orig, categories => \@tags, }, $u ); ok( $atom_entry_server->published eq $atom_entry_server->updated, "same publish and edit date" ); ok( !$atom_entry_server->summary, "no summary; we have the content." ); note("List entries"); do_request( GET => $u->atom_base . "/entries" ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Entries feed needs authorization." ); do_request( GET => $u->atom_base . "/entries", authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Retrieved entry list" ); is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" ); my $feed = XML::Atom::Feed->new( \$r->response_content ); my @entries = $feed->entries; is( scalar @entries, 1, "Got entry from feed." ); note("Retrieve entry"); do_request( GET => $u->atom_base . "/entries/1" ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Retrieving entry needs authorization." ); do_request( GET => $u->atom_base . "/entries/12345", authenticate => 1 ); is( $r->status, $r->NOT_FOUND, "No such entry" ); is( $r->content_type, "text/plain", "AtomAPI entry content type" ); do_request( POST => $u->atom_base . "/entries/1", authenticate => 1 ); is( $r->status, $r->HTTP_METHOD_NOT_ALLOWED, $u->atom_base . "/entries/1 does not support POST." ); do_request( GET => $u->atom_base . "/entries/1", authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Retrieved entry" ); is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" ); $atom_entry_server = XML::Atom::Entry->new( \$r->response_content ); check_entry( $atom_entry_server, { id => $entry_obj->jitemid, title => $entry_obj->subject_raw, content => $entry_obj->event_raw, atom_id => $entry_obj->atom_id, url => $entry_obj->url, author => $u->name_orig, categories => \@tags }, $u ); ok( $atom_entry_server->published eq $atom_entry_server->updated, "same publish and edit date" ); ok( !$atom_entry_server->summary, "no summary; we have the content." ); note("Edit entry"); do_request( PUT => $u->atom_base . "/entries/1" ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Retrieving entry needs authorization." ); do_request( PUT => $u->atom_base . "/entries/12345", authenticate => 1 ); is( $r->status, $r->NOT_FOUND, "No such entry" ); is( $r->content_type, "text/plain", "AtomAPI entry content type" ); $atom_entry = XML::Atom::Entry->new( Version => 1 ); $title = "Edited Post"; $content = "Content of my post at " . rand(); @tags = qw( foo2 bar2 ); $atom_entry->id( $atom_entry_server->id ); $atom_entry->title($title); $atom_entry->content($content); foreach my $tag (@tags) { my $category = XML::Atom::Category->new( Version => 1 ); $category->term($tag); $atom_entry->add_category($category); } # put a little bit of time between publish and update sleep(1); do_request( PUT => $u->atom_base . "/entries/1", authenticate => 1, data => { input => $atom_entry->as_xml } ); is( $r->status, $r->HTTP_OK, "Edited entry" ); is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" ); do_request( GET => $u->atom_base . "/entries/1", authenticate => 1 ); $atom_entry_server = XML::Atom::Entry->new( \$r->response_content ); check_entry( $atom_entry_server, { id => $entry_obj->jitemid, title => $title, content => $content, atom_id => $entry_obj->atom_id, url => $entry_obj->url, author => $u->name_orig, categories => \@tags }, $u ); ok( $atom_entry_server->published ne $atom_entry_server->updated, "different publish and edit date" ); $atom_entry_server->id("123"); do_request( PUT => $u->atom_base . "/entries/1", authenticate => 1, data => { input => $atom_entry_server->as_xml } ); is( $r->status, $r->HTTP_BAD_REQUEST, "Mismatched ids" ); do_request( DELETE => $u->atom_base . "/entries/1", authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Deleted entry" ); $entry_obj = LJ::Entry->new( $u, jitemid => 1 ); isnt( $entry_obj->valid, "Entry confirmed deleted" ); do_request( PUT => $u->atom_base . "/entries/1", authenticate => 1 ); is( $r->status, $r->NOT_FOUND, "Trying to edit deleted entry" ); note("Checking community functionality."); { my $memberof_cu = temp_comm(); my $nonmemberof_cu = temp_comm(); $u->join_community( $memberof_cu, 1, 1 ); my $another_u = temp_user(); # another member of the community $another_u->set_password($pass); $another_u->join_community( $memberof_cu, 1, 1 ); my $admin_u = temp_user(); # an administrator of the community $admin_u->set_password($pass); $admin_u->join_community( $memberof_cu, 1, 1 ); LJ::set_rel( $memberof_cu->userid, $admin_u->userid, "A" ); note("Service document introspection (community)."); # unauthenticated to community do_request( GET => $memberof_cu->atom_service_document ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Service document protected by authorization." ); # community you aren't a member of do_request( GET => $nonmemberof_cu->atom_service_document, authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Not a member of the community, but we still get the service document for the user (which doesn't contain the community)." ); SKIP: { skip "No XML::Atom::Service/XML::Atom::Categories module installed.", 3 unless eval "use XML::Atom::Service; use XML::Atom::Categories; 1;"; my $service_document_xml = $r->response_content; my $service = XML::Atom::Service->new( \$service_document_xml ); ok( $service, "Got service document." ); my @workspaces = $service->workspace; is( scalar @workspaces, 2, "One workspace" ); isnt( $_->title, $nonmemberof_cu->user, "Community you're not a member of doesn't appear in the service document." ) foreach @workspaces; } # community you are a member of do_request( GET => $memberof_cu->atom_service_document, authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Got service document." ); like( $r->content_type, qr#^\Qapplication/atomsvc+xml\E#, "Service content type" ); SKIP: { skip "No XML::Atom::Service/XML::Atom::Categories module installed.", 8 unless eval "use XML::Atom::Service; use XML::Atom::Categories; 1;"; my $service_document_xml = $r->response_content; my $service = XML::Atom::Service->new( \$service_document_xml ); ok( $service, "Got service document." ); my @workspaces = $service->workspace; is( scalar @workspaces, 2, "Personal journal and community as separate workspaces" ); # making assumptions that the second workspace is our community my $memberof_cu_workspace = $workspaces[1]; is( $memberof_cu_workspace->title, $memberof_cu->user, "Workspace title" ); my @collections = $memberof_cu_workspace->collections; is( scalar @collections, 1, "One collection" ); is( $collections[0]->title, "Entries", "Entries collection title" ); is( $collections[0]->href, $memberof_cu->atom_base . "/entries", "Entries collection uri" ); my @categories = $collections[0]->categories; is( scalar @categories, 1, "One categories link" ); is( $categories[0]->href, $memberof_cu->atom_base . "/entries/tags", "Categories collection uri" ); } note("Create an entry (community)."); my $title = "Community entry"; my $content = "Community entry content " . rand(); my $atom_entry = XML::Atom::Entry->new( Version => 1 ); $atom_entry->title($title); $atom_entry->content($content); # unauthenticated to community do_request( POST => $memberof_cu->atom_base . "/entries", data => { input => $atom_entry->as_xml } ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Trying to post to community while unauthenticated." ); # community you don't have posting access to do_request( POST => $nonmemberof_cu->atom_base . "/entries", authenticate => 1, data => { input => $atom_entry->as_xml } ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Trying to post to community, but don't have posting access." ); # community you have posting access to do_request( POST => $memberof_cu->atom_base . "/entries", authenticate => 1, data => { input => $atom_entry->as_xml } ); is( $r->status, $r->HTTP_CREATED, "POSTed new entry" ); is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" ); note("Double-check posted entry (community)."); $entry_obj = LJ::Entry->new( $memberof_cu, jitemid => 1 ); ok( $entry_obj, "got entry" ); ok( $entry_obj->valid, "entry is valid" ); is( $entry_obj->subject_raw, $title, "item has right title" ); is( $entry_obj->event_raw, $content, "item has right content" ); $atom_entry_server = XML::Atom::Entry->new( \$r->response_content ); check_entry( $atom_entry_server, { id => $entry_obj->jitemid, title => $atom_entry_server->title, content => $atom_entry_server->content->body, url => $entry_obj->url, author => $u->name_orig, }, $memberof_cu ); note("List entries (community)."); # unauthenticated to community do_request( GET => $memberof_cu->atom_base . "/entries" ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Entries feed needs authorization." ); # community you don't have posting access to do_request( GET => $nonmemberof_cu->atom_base . "/entries", authenticate => 1 ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Entries feed needs authorization." ); # community you have posting access to do_request( GET => $memberof_cu->atom_base . "/entries", authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Retrieved entry list" ); is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" ); my $feed = XML::Atom::Feed->new( \$r->response_content ); my @entries = $feed->entries; is( scalar @entries, 1, "Got entry from feed." ); note("Retrieve entry (community)"); # unauthenticated to community do_request( GET => $memberof_cu->atom_base . "/entries/1" ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Retrieving entry needs authorization." ); # community you don't have posting access to do_request( GET => $nonmemberof_cu->atom_base . "/entries/1", authenticate => 1 ); is( $r->status, $r->HTTP_UNAUTHORIZED, "Retrieving entry needs authorization." ); # community you have posting access to # retrieve (should succeed) # edit (should succeed) # delete (should succeed) do_request( GET => $memberof_cu->atom_base . "/entries/1", authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Retrieved entry" ); is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" ); $atom_entry_server = XML::Atom::Entry->new( \$r->response_content ); check_entry( $atom_entry_server, { id => $entry_obj->jitemid, title => $entry_obj->subject_raw, content => $entry_obj->event_raw, atom_id => $entry_obj->atom_id, url => $entry_obj->url, author => $u->name_orig }, $memberof_cu ); $atom_entry = XML::Atom::Entry->new( Version => 1 ); $title = "Edited Post"; $content = "Content of my post at " . rand(); $atom_entry->id( $atom_entry_server->id ); $atom_entry->title($title); $atom_entry->content($content); do_request( PUT => $memberof_cu->atom_base . "/entries/1", authenticate => 1, data => { input => $atom_entry->as_xml } ); is( $r->status, $r->HTTP_OK, "Edited entry" ); is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" ); do_request( GET => $memberof_cu->atom_base . "/entries/1", authenticate => 1 ); $atom_entry_server = XML::Atom::Entry->new( \$r->response_content ); check_entry( $atom_entry_server, { id => $entry_obj->jitemid, title => $title, content => $content, atom_id => $entry_obj->atom_id, url => $entry_obj->url, author => $u->name_orig, }, $memberof_cu ); do_request( DELETE => $memberof_cu->atom_base . "/entries/1", authenticate => 1 ); is( $r->status, $r->HTTP_OK, "Deleted entry" ); $entry_obj = LJ::Entry->new( $memberof_cu, jitemid => 1 ); isnt( $entry_obj->valid, "Entry confirmed deleted" ); note("Check what other people can do."); # make another entry that other people can view/manipulate do_request( POST => $memberof_cu->atom_base . "/entries", authenticate => 1, data => { input => $atom_entry->as_xml } ); $atom_entry_server = XML::Atom::Entry->new( \$r->response_content ); $atom_entry = XML::Atom::Entry->new( Version => 1 ); $title = "Edited Post"; $content = "Content of my post at " . rand(); $atom_entry->id( $atom_entry_server->id ); $atom_entry->title($title); $atom_entry->content($content); # another community member # retrieve (should fail) # edit (should fail) # delete (should fail) do_request( GET => $memberof_cu->atom_base . "/entries/2", authenticate => 1, remote => $another_u ); is( $r->status, $r->HTTP_UNAUTHORIZED, "You don't own this entry (another_u, get)" ); do_request( PUT => $memberof_cu->atom_base . "/entries/2", authenticate => 1, remote => $another_u, data => { input => $atom_entry->as_xml } ); is( $r->status, $r->HTTP_UNAUTHORIZED, "You don't own this entry (another_u, edit)" ); do_request( DELETE => $memberof_cu->atom_base . "/entries/2", authenticate => 1, remote => $another_u ); is( $r->status, $r->HTTP_UNAUTHORIZED, "You don't own this entry (another_u, delete)" ); # community admin # retrieve (should succeed) # edit (should fail) # delete (should succeed) do_request( GET => $memberof_cu->atom_base . "/entries/2", authenticate => 1, remote => $admin_u ); is( $r->status, $r->HTTP_OK, "Retrieved entry" ); is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" ); do_request( PUT => $memberof_cu->atom_base . "/entries/2", authenticate => 1, remote => $admin_u, data => { input => $atom_entry->as_xml } ); is( $r->status, $r->HTTP_UNAUTHORIZED, "You don't own this entry (admin_u, edit)" ); do_request( DELETE => $memberof_cu->atom_base . "/entries/2", authenticate => 1, remote => $admin_u ); is( $r->status, $r->HTTP_OK, "Deleted entry (admin_u, delete)" ); $entry_obj = LJ::Entry->new( $memberof_cu, jitemid => 2 ); isnt( $entry_obj->valid, "Entry confirmed deleted" ); } done_testing();